1#![allow(clippy::redundant_closure_for_method_calls)]
7#![allow(clippy::uninlined_format_args)]
8#![allow(clippy::match_same_arms)]
9#![allow(clippy::format_collect)]
10#![allow(clippy::inefficient_to_string)]
11
12use super::GeneratedCode;
13use crate::Language;
14
15#[derive(Debug, Clone, PartialEq)]
17#[allow(missing_docs)]
18pub enum BashNode {
19 Script {
21 shebang: Option<String>,
23 body: Vec<BashNode>,
25 },
26 Assignment {
28 name: String,
30 value: Box<BashNode>,
32 },
33 Command {
35 name: String,
37 args: Vec<BashNode>,
39 },
40 If {
42 condition: Box<BashNode>,
44 then_body: Vec<BashNode>,
46 else_body: Vec<BashNode>,
48 },
49 For {
51 var: String,
53 items: Vec<BashNode>,
55 body: Vec<BashNode>,
57 },
58 While {
60 condition: Box<BashNode>,
62 body: Vec<BashNode>,
64 },
65 Function {
67 name: String,
69 body: Vec<BashNode>,
71 },
72 Case {
74 value: Box<BashNode>,
76 patterns: Vec<(String, Vec<BashNode>)>,
78 },
79 Test {
81 double: bool,
83 expr: Box<BashNode>,
85 },
86 Compare {
88 left: Box<BashNode>,
90 op: BashCompareOp,
92 right: Box<BashNode>,
94 },
95 Arithmetic(Box<BashNode>),
97 ArithOp {
99 left: Box<BashNode>,
101 op: BashArithOp,
103 right: Box<BashNode>,
105 },
106 Variable(String),
108 StringLit(String),
110 IntLit(i64),
112 Array(Vec<BashNode>),
114 Pipe {
116 left: Box<BashNode>,
118 right: Box<BashNode>,
120 },
121 CommandSubst(Box<BashNode>),
123 Redirect {
125 command: Box<BashNode>,
127 redirect_type: RedirectType,
129 target: String,
131 },
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum BashCompareOp {
137 NumEq,
139 NumNe,
141 NumLt,
143 NumGt,
145 NumLe,
147 NumGe,
149 StrEq,
151 StrNe,
153 StrLt,
155 StrGt,
157}
158
159impl BashCompareOp {
160 #[must_use]
162 pub fn all() -> &'static [Self] {
163 &[
164 Self::NumEq,
165 Self::NumNe,
166 Self::NumLt,
167 Self::NumGt,
168 Self::NumLe,
169 Self::NumGe,
170 Self::StrEq,
171 Self::StrNe,
172 ]
173 }
174
175 #[must_use]
177 pub fn to_str(self) -> &'static str {
178 match self {
179 Self::NumEq => "-eq",
180 Self::NumNe => "-ne",
181 Self::NumLt => "-lt",
182 Self::NumGt => "-gt",
183 Self::NumLe => "-le",
184 Self::NumGe => "-ge",
185 Self::StrEq => "==",
186 Self::StrNe => "!=",
187 Self::StrLt => "<",
188 Self::StrGt => ">",
189 }
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum BashArithOp {
196 Add,
198 Sub,
200 Mult,
202 Div,
204 Mod,
206}
207
208impl BashArithOp {
209 #[must_use]
211 pub fn all() -> &'static [Self] {
212 &[Self::Add, Self::Sub, Self::Mult, Self::Div, Self::Mod]
213 }
214
215 #[must_use]
217 pub fn to_str(self) -> &'static str {
218 match self {
219 Self::Add => "+",
220 Self::Sub => "-",
221 Self::Mult => "*",
222 Self::Div => "/",
223 Self::Mod => "%",
224 }
225 }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum RedirectType {
231 Output,
233 Append,
235 Input,
237 StderrToStdout,
239}
240
241impl RedirectType {
242 #[must_use]
244 pub fn to_str(self) -> &'static str {
245 match self {
246 Self::Output => ">",
247 Self::Append => ">>",
248 Self::Input => "<",
249 Self::StderrToStdout => "2>&1",
250 }
251 }
252}
253
254impl BashNode {
255 #[must_use]
257 #[allow(clippy::too_many_lines)]
258 pub fn to_code(&self) -> String {
259 match self {
260 Self::Script { shebang, body } => {
261 let mut code = String::new();
262 if let Some(s) = shebang {
263 code.push_str(s);
264 code.push('\n');
265 }
266 for stmt in body {
267 code.push_str(&stmt.to_code());
268 code.push('\n');
269 }
270 code.trim_end().to_string()
271 }
272 Self::Assignment { name, value } => {
273 format!("{}={}", name, value.to_code())
274 }
275 Self::Command { name, args } => {
276 if args.is_empty() {
277 name.clone()
278 } else {
279 let args_str: Vec<String> = args.iter().map(|a| a.to_code()).collect();
280 format!("{} {}", name, args_str.join(" "))
281 }
282 }
283 Self::If {
284 condition,
285 then_body,
286 else_body,
287 } => {
288 let cond = condition.to_code();
289 let then_str: String = then_body
290 .iter()
291 .map(|s| format!(" {}", s.to_code()))
292 .collect::<Vec<_>>()
293 .join("\n");
294
295 if else_body.is_empty() {
296 format!("if {}; then\n{}\nfi", cond, then_str)
297 } else {
298 let else_str: String = else_body
299 .iter()
300 .map(|s| format!(" {}", s.to_code()))
301 .collect::<Vec<_>>()
302 .join("\n");
303 format!("if {}; then\n{}\nelse\n{}\nfi", cond, then_str, else_str)
304 }
305 }
306 Self::For { var, items, body } => {
307 let items_str: Vec<String> = items.iter().map(|i| i.to_code()).collect();
308 let body_str: String = body
309 .iter()
310 .map(|s| format!(" {}", s.to_code()))
311 .collect::<Vec<_>>()
312 .join("\n");
313 format!(
314 "for {} in {}; do\n{}\ndone",
315 var,
316 items_str.join(" "),
317 body_str
318 )
319 }
320 Self::While { condition, body } => {
321 let body_str: String = body
322 .iter()
323 .map(|s| format!(" {}", s.to_code()))
324 .collect::<Vec<_>>()
325 .join("\n");
326 format!("while {}; do\n{}\ndone", condition.to_code(), body_str)
327 }
328 Self::Function { name, body } => {
329 let body_str: String = body
330 .iter()
331 .map(|s| format!(" {}", s.to_code()))
332 .collect::<Vec<_>>()
333 .join("\n");
334 format!("{}() {{\n{}\n}}", name, body_str)
335 }
336 Self::Case { value, patterns } => {
337 let patterns_str: String = patterns
338 .iter()
339 .map(|(pat, body)| {
340 let body_str: String = body
341 .iter()
342 .map(|s| s.to_code())
343 .collect::<Vec<_>>()
344 .join("; ");
345 format!(" {}) {};;\n", pat, body_str)
346 })
347 .collect();
348 format!("case {} in\n{}esac", value.to_code(), patterns_str)
349 }
350 Self::Test { double, expr } => {
351 if *double {
352 format!("[[ {} ]]", expr.to_code())
353 } else {
354 format!("[ {} ]", expr.to_code())
355 }
356 }
357 Self::Compare { left, op, right } => {
358 format!("{} {} {}", left.to_code(), op.to_str(), right.to_code())
359 }
360 Self::Arithmetic(expr) => {
361 format!("$(({}))", expr.to_code())
362 }
363 Self::ArithOp { left, op, right } => {
364 format!("{} {} {}", left.to_code(), op.to_str(), right.to_code())
365 }
366 Self::Variable(name) => format!("${}", name),
367 Self::StringLit(s) => {
368 if s.contains(' ') || s.contains('$') {
369 format!("\"{}\"", s)
370 } else {
371 s.clone()
372 }
373 }
374 Self::IntLit(n) => n.to_string(),
375 Self::Array(items) => {
376 let items_str: Vec<String> = items.iter().map(|i| i.to_code()).collect();
377 format!("({})", items_str.join(" "))
378 }
379 Self::Pipe { left, right } => {
380 format!("{} | {}", left.to_code(), right.to_code())
381 }
382 Self::CommandSubst(cmd) => {
383 format!("$({})", cmd.to_code())
384 }
385 Self::Redirect {
386 command,
387 redirect_type,
388 target,
389 } => {
390 format!(
391 "{} {} {}",
392 command.to_code(),
393 redirect_type.to_str(),
394 target
395 )
396 }
397 }
398 }
399
400 #[must_use]
402 pub fn depth(&self) -> usize {
403 match self {
404 Self::Script { body, .. } => 1 + body.iter().map(Self::depth).max().unwrap_or(0),
405 Self::Assignment { value, .. } => 1 + value.depth(),
406 Self::Command { args, .. } => 1 + args.iter().map(Self::depth).max().unwrap_or(0),
407 Self::If {
408 condition,
409 then_body,
410 else_body,
411 } => {
412 let cond_depth = condition.depth();
413 let then_depth = then_body.iter().map(Self::depth).max().unwrap_or(0);
414 let else_depth = else_body.iter().map(Self::depth).max().unwrap_or(0);
415 1 + cond_depth.max(then_depth).max(else_depth)
416 }
417 Self::For { items, body, .. } => {
418 let items_depth = items.iter().map(Self::depth).max().unwrap_or(0);
419 let body_depth = body.iter().map(Self::depth).max().unwrap_or(0);
420 1 + items_depth.max(body_depth)
421 }
422 Self::While { condition, body } => {
423 let cond_depth = condition.depth();
424 let body_depth = body.iter().map(Self::depth).max().unwrap_or(0);
425 1 + cond_depth.max(body_depth)
426 }
427 Self::Function { body, .. } => 1 + body.iter().map(Self::depth).max().unwrap_or(0),
428 Self::Case { value, patterns } => {
429 let val_depth = value.depth();
430 let pat_depth = patterns
431 .iter()
432 .flat_map(|(_, body)| body.iter().map(Self::depth))
433 .max()
434 .unwrap_or(0);
435 1 + val_depth.max(pat_depth)
436 }
437 Self::Test { expr, .. } => 1 + expr.depth(),
438 Self::Compare { left, right, .. } => 1 + left.depth().max(right.depth()),
439 Self::Arithmetic(expr) => 1 + expr.depth(),
440 Self::ArithOp { left, right, .. } => 1 + left.depth().max(right.depth()),
441 Self::Pipe { left, right } => 1 + left.depth().max(right.depth()),
442 Self::CommandSubst(cmd) => 1 + cmd.depth(),
443 Self::Redirect { command, .. } => 1 + command.depth(),
444 Self::Variable(_) | Self::StringLit(_) | Self::IntLit(_) => 1,
445 Self::Array(items) => 1 + items.iter().map(Self::depth).max().unwrap_or(0),
446 }
447 }
448
449 #[must_use]
451 pub fn features(&self) -> Vec<String> {
452 let mut features = Vec::new();
453
454 match self {
455 Self::Script { body, .. } => {
456 features.push("script".to_string());
457 for stmt in body {
458 features.extend(stmt.features());
459 }
460 }
461 Self::Assignment { .. } => features.push("assignment".to_string()),
462 Self::Command { name, .. } => {
463 features.push("command".to_string());
464 features.push(format!("cmd_{}", name));
465 }
466 Self::If { .. } => features.push("if".to_string()),
467 Self::For { .. } => features.push("for".to_string()),
468 Self::While { .. } => features.push("while".to_string()),
469 Self::Function { .. } => features.push("function".to_string()),
470 Self::Case { .. } => features.push("case".to_string()),
471 Self::Test { double, .. } => {
472 features.push(
473 if *double {
474 "test_double"
475 } else {
476 "test_single"
477 }
478 .to_string(),
479 );
480 }
481 Self::Compare { op, .. } => features.push(format!("compare_{}", op.to_str())),
482 Self::Arithmetic(_) => features.push("arithmetic".to_string()),
483 Self::ArithOp { op, .. } => features.push(format!("arith_{}", op.to_str())),
484 Self::Variable(_) => features.push("variable".to_string()),
485 Self::StringLit(_) => features.push("string".to_string()),
486 Self::IntLit(_) => features.push("integer".to_string()),
487 Self::Array(_) => features.push("array".to_string()),
488 Self::Pipe { .. } => features.push("pipe".to_string()),
489 Self::CommandSubst(_) => features.push("command_subst".to_string()),
490 Self::Redirect { redirect_type, .. } => {
491 features.push(format!("redirect_{}", redirect_type.to_str()));
492 }
493 }
494
495 features
496 }
497}
498
499#[derive(Debug)]
501pub struct BashEnumerator {
502 max_depth: usize,
504}
505
506impl BashEnumerator {
507 #[must_use]
509 pub fn new(max_depth: usize) -> Self {
510 Self { max_depth }
511 }
512
513 #[must_use]
515 #[allow(clippy::too_many_lines)]
516 pub fn enumerate_programs(&self) -> Vec<GeneratedCode> {
517 let mut programs = Vec::new();
518
519 for var in &["x", "y", "result"] {
521 for val in [1, 0, 42] {
522 let node = BashNode::Assignment {
523 name: var.to_string(),
524 value: Box::new(BashNode::IntLit(val)),
525 };
526 if node.depth() <= self.max_depth {
527 programs.push(self.node_to_generated(&node));
528 }
529 }
530 for s in &["hello", "world"] {
532 let node = BashNode::Assignment {
533 name: var.to_string(),
534 value: Box::new(BashNode::StringLit(s.to_string())),
535 };
536 if node.depth() <= self.max_depth {
537 programs.push(self.node_to_generated(&node));
538 }
539 }
540 }
541
542 for arg in &["$x", "hello", "$HOME"] {
544 let node = BashNode::Command {
545 name: "echo".to_string(),
546 args: vec![BashNode::StringLit(arg.to_string())],
547 };
548 if node.depth() <= self.max_depth {
549 programs.push(self.node_to_generated(&node));
550 }
551 }
552
553 if self.max_depth >= 2 {
555 for var in &["x", "y"] {
556 for op in &[
557 BashCompareOp::NumEq,
558 BashCompareOp::NumGt,
559 BashCompareOp::NumLt,
560 ] {
561 let node = BashNode::If {
562 condition: Box::new(BashNode::Test {
563 double: false,
564 expr: Box::new(BashNode::Compare {
565 left: Box::new(BashNode::Variable(var.to_string())),
566 op: *op,
567 right: Box::new(BashNode::IntLit(0)),
568 }),
569 }),
570 then_body: vec![BashNode::Command {
571 name: "echo".to_string(),
572 args: vec![BashNode::StringLit("yes".to_string())],
573 }],
574 else_body: vec![],
575 };
576 if node.depth() <= self.max_depth {
577 programs.push(self.node_to_generated(&node));
578 }
579 }
580 }
581 }
582
583 if self.max_depth >= 2 {
585 let node = BashNode::For {
586 var: "i".to_string(),
587 items: vec![
588 BashNode::IntLit(1),
589 BashNode::IntLit(2),
590 BashNode::IntLit(3),
591 ],
592 body: vec![BashNode::Command {
593 name: "echo".to_string(),
594 args: vec![BashNode::Variable("i".to_string())],
595 }],
596 };
597 if node.depth() <= self.max_depth {
598 programs.push(self.node_to_generated(&node));
599 }
600 }
601
602 if self.max_depth >= 2 {
604 let node = BashNode::While {
605 condition: Box::new(BashNode::Test {
606 double: false,
607 expr: Box::new(BashNode::Compare {
608 left: Box::new(BashNode::Variable("x".to_string())),
609 op: BashCompareOp::NumGt,
610 right: Box::new(BashNode::IntLit(0)),
611 }),
612 }),
613 body: vec![BashNode::Assignment {
614 name: "x".to_string(),
615 value: Box::new(BashNode::Arithmetic(Box::new(BashNode::ArithOp {
616 left: Box::new(BashNode::Variable("x".to_string())),
617 op: BashArithOp::Sub,
618 right: Box::new(BashNode::IntLit(1)),
619 }))),
620 }],
621 };
622 if node.depth() <= self.max_depth {
623 programs.push(self.node_to_generated(&node));
624 }
625 }
626
627 if self.max_depth >= 2 {
629 for name in &["greet", "main"] {
630 let node = BashNode::Function {
631 name: name.to_string(),
632 body: vec![BashNode::Command {
633 name: "echo".to_string(),
634 args: vec![BashNode::StringLit("hello".to_string())],
635 }],
636 };
637 if node.depth() <= self.max_depth {
638 programs.push(self.node_to_generated(&node));
639 }
640 }
641 }
642
643 for op in BashArithOp::all() {
645 let node = BashNode::Assignment {
646 name: "result".to_string(),
647 value: Box::new(BashNode::Arithmetic(Box::new(BashNode::ArithOp {
648 left: Box::new(BashNode::IntLit(1)),
649 op: *op,
650 right: Box::new(BashNode::IntLit(2)),
651 }))),
652 };
653 if node.depth() <= self.max_depth {
654 programs.push(self.node_to_generated(&node));
655 }
656 }
657
658 if self.max_depth >= 2 {
660 let node = BashNode::Pipe {
661 left: Box::new(BashNode::Command {
662 name: "echo".to_string(),
663 args: vec![BashNode::StringLit("hello".to_string())],
664 }),
665 right: Box::new(BashNode::Command {
666 name: "wc".to_string(),
667 args: vec![BashNode::StringLit("-c".to_string())],
668 }),
669 };
670 if node.depth() <= self.max_depth {
671 programs.push(self.node_to_generated(&node));
672 }
673 }
674
675 let node = BashNode::Assignment {
677 name: "arr".to_string(),
678 value: Box::new(BashNode::Array(vec![
679 BashNode::IntLit(1),
680 BashNode::IntLit(2),
681 BashNode::IntLit(3),
682 ])),
683 };
684 if node.depth() <= self.max_depth {
685 programs.push(self.node_to_generated(&node));
686 }
687
688 let node = BashNode::Assignment {
690 name: "output".to_string(),
691 value: Box::new(BashNode::CommandSubst(Box::new(BashNode::Command {
692 name: "echo".to_string(),
693 args: vec![BashNode::StringLit("hello".to_string())],
694 }))),
695 };
696 if node.depth() <= self.max_depth {
697 programs.push(self.node_to_generated(&node));
698 }
699
700 for redirect_type in &[
702 RedirectType::Output,
703 RedirectType::Append,
704 RedirectType::Input,
705 ] {
706 let target = match redirect_type {
707 RedirectType::Input => "/dev/null",
708 _ => "/tmp/output.txt",
709 };
710 let node = BashNode::Redirect {
711 command: Box::new(BashNode::Command {
712 name: "echo".to_string(),
713 args: vec![BashNode::StringLit("test".to_string())],
714 }),
715 redirect_type: *redirect_type,
716 target: target.to_string(),
717 };
718 if node.depth() <= self.max_depth {
719 programs.push(self.node_to_generated(&node));
720 }
721 }
722
723 for cmd in &["cat", "test", "true", "false", "pwd", "ls"] {
725 let node = BashNode::Command {
726 name: cmd.to_string(),
727 args: vec![],
728 };
729 if node.depth() <= self.max_depth {
730 programs.push(self.node_to_generated(&node));
731 }
732 }
733
734 if self.max_depth >= 2 {
736 for var in &["x", "str"] {
737 let node = BashNode::If {
738 condition: Box::new(BashNode::Test {
739 double: true,
740 expr: Box::new(BashNode::Compare {
741 left: Box::new(BashNode::Variable(var.to_string())),
742 op: BashCompareOp::StrEq,
743 right: Box::new(BashNode::StringLit("hello".to_string())),
744 }),
745 }),
746 then_body: vec![BashNode::Command {
747 name: "echo".to_string(),
748 args: vec![BashNode::StringLit("match".to_string())],
749 }],
750 else_body: vec![],
751 };
752 if node.depth() <= self.max_depth {
753 programs.push(self.node_to_generated(&node));
754 }
755 }
756 }
757
758 if self.max_depth >= 2 {
760 let node = BashNode::If {
761 condition: Box::new(BashNode::Test {
762 double: false,
763 expr: Box::new(BashNode::Compare {
764 left: Box::new(BashNode::Variable("x".to_string())),
765 op: BashCompareOp::NumEq,
766 right: Box::new(BashNode::IntLit(1)),
767 }),
768 }),
769 then_body: vec![BashNode::Command {
770 name: "echo".to_string(),
771 args: vec![BashNode::StringLit("one".to_string())],
772 }],
773 else_body: vec![BashNode::Command {
774 name: "echo".to_string(),
775 args: vec![BashNode::StringLit("not one".to_string())],
776 }],
777 };
778 if node.depth() <= self.max_depth {
779 programs.push(self.node_to_generated(&node));
780 }
781 }
782
783 if self.max_depth >= 2 {
785 let node = BashNode::Case {
786 value: Box::new(BashNode::Variable("x".to_string())),
787 patterns: vec![
788 (
789 "1".to_string(),
790 vec![BashNode::Command {
791 name: "echo".to_string(),
792 args: vec![BashNode::StringLit("one".to_string())],
793 }],
794 ),
795 (
796 "2".to_string(),
797 vec![BashNode::Command {
798 name: "echo".to_string(),
799 args: vec![BashNode::StringLit("two".to_string())],
800 }],
801 ),
802 (
803 "*".to_string(),
804 vec![BashNode::Command {
805 name: "echo".to_string(),
806 args: vec![BashNode::StringLit("other".to_string())],
807 }],
808 ),
809 ],
810 };
811 if node.depth() <= self.max_depth {
812 programs.push(self.node_to_generated(&node));
813 }
814 }
815
816 if self.max_depth >= 2 {
818 for op in &[BashCompareOp::StrEq, BashCompareOp::StrNe] {
819 let node = BashNode::If {
820 condition: Box::new(BashNode::Test {
821 double: true,
822 expr: Box::new(BashNode::Compare {
823 left: Box::new(BashNode::Variable("str".to_string())),
824 op: *op,
825 right: Box::new(BashNode::StringLit("test".to_string())),
826 }),
827 }),
828 then_body: vec![BashNode::Command {
829 name: "echo".to_string(),
830 args: vec![BashNode::StringLit("matched".to_string())],
831 }],
832 else_body: vec![],
833 };
834 if node.depth() <= self.max_depth {
835 programs.push(self.node_to_generated(&node));
836 }
837 }
838 }
839
840 if self.max_depth >= 2 {
842 let node = BashNode::For {
843 var: "f".to_string(),
844 items: vec![BashNode::StringLit("*.txt".to_string())],
845 body: vec![BashNode::Command {
846 name: "echo".to_string(),
847 args: vec![BashNode::Variable("f".to_string())],
848 }],
849 };
850 if node.depth() <= self.max_depth {
851 programs.push(self.node_to_generated(&node));
852 }
853 }
854
855 if self.max_depth >= 3 {
857 let node = BashNode::Pipe {
858 left: Box::new(BashNode::Command {
859 name: "cat".to_string(),
860 args: vec![BashNode::StringLit("/etc/passwd".to_string())],
861 }),
862 right: Box::new(BashNode::Pipe {
863 left: Box::new(BashNode::Command {
864 name: "grep".to_string(),
865 args: vec![BashNode::StringLit("root".to_string())],
866 }),
867 right: Box::new(BashNode::Command {
868 name: "wc".to_string(),
869 args: vec![BashNode::StringLit("-l".to_string())],
870 }),
871 }),
872 };
873 if node.depth() <= self.max_depth {
874 programs.push(self.node_to_generated(&node));
875 }
876 }
877
878 let vars = [
882 "x", "y", "z", "a", "b", "n", "i", "j", "k", "count", "sum", "result", "tmp", "val",
883 "num",
884 ];
885 let ints = [0, 1, 2, 3, 5, 10, 42, 100, 255, -1];
886 let strings = [
887 "hello", "world", "test", "foo", "bar", "baz", "value", "data", "file", "",
888 ];
889
890 for var in &vars {
892 for val in &ints {
893 let node = BashNode::Assignment {
894 name: var.to_string(),
895 value: Box::new(BashNode::IntLit(*val)),
896 };
897 if node.depth() <= self.max_depth {
898 programs.push(self.node_to_generated(&node));
899 }
900 }
901 }
902
903 for var in &vars {
905 for s in &strings {
906 let node = BashNode::Assignment {
907 name: var.to_string(),
908 value: Box::new(BashNode::StringLit(s.to_string())),
909 };
910 if node.depth() <= self.max_depth {
911 programs.push(self.node_to_generated(&node));
912 }
913 }
914 }
915
916 let cmds = [
918 "echo", "printf", "cat", "ls", "pwd", "cd", "mkdir", "rm", "cp", "mv", "grep", "sed",
919 "awk", "cut", "sort", "uniq", "wc", "head", "tail", "tee", "true", "false", "test",
920 "exit", "return", "break", "continue", "read", "export", "unset", "local", "declare",
921 "typeset", "readonly",
922 ];
923
924 for cmd in &cmds {
925 let node = BashNode::Command {
927 name: cmd.to_string(),
928 args: vec![],
929 };
930 if node.depth() <= self.max_depth {
931 programs.push(self.node_to_generated(&node));
932 }
933
934 for arg in &[
936 "$x",
937 "$1",
938 "$@",
939 "-n",
940 "-e",
941 "-r",
942 "-f",
943 "file.txt",
944 "/dev/null",
945 ] {
946 let node = BashNode::Command {
947 name: cmd.to_string(),
948 args: vec![BashNode::StringLit(arg.to_string())],
949 };
950 if node.depth() <= self.max_depth {
951 programs.push(self.node_to_generated(&node));
952 }
953 }
954 }
955
956 if self.max_depth >= 2 {
958 for var in &["x", "y", "z", "n", "count"] {
959 for op in BashCompareOp::all() {
960 for right_val in &[0, 1, 10] {
961 let node = BashNode::If {
963 condition: Box::new(BashNode::Test {
964 double: false,
965 expr: Box::new(BashNode::Compare {
966 left: Box::new(BashNode::Variable(var.to_string())),
967 op: *op,
968 right: Box::new(BashNode::IntLit(*right_val)),
969 }),
970 }),
971 then_body: vec![BashNode::Command {
972 name: "echo".to_string(),
973 args: vec![BashNode::StringLit("true".to_string())],
974 }],
975 else_body: vec![],
976 };
977 if node.depth() <= self.max_depth {
978 programs.push(self.node_to_generated(&node));
979 }
980
981 let node = BashNode::If {
983 condition: Box::new(BashNode::Test {
984 double: true,
985 expr: Box::new(BashNode::Compare {
986 left: Box::new(BashNode::Variable(var.to_string())),
987 op: *op,
988 right: Box::new(BashNode::IntLit(*right_val)),
989 }),
990 }),
991 then_body: vec![BashNode::Command {
992 name: "echo".to_string(),
993 args: vec![BashNode::StringLit("true".to_string())],
994 }],
995 else_body: vec![],
996 };
997 if node.depth() <= self.max_depth {
998 programs.push(self.node_to_generated(&node));
999 }
1000 }
1001 }
1002 }
1003 }
1004
1005 for left_val in &[0, 1, 2, 5, 10] {
1007 for right_val in &[1, 2, 3, 5] {
1008 for op in BashArithOp::all() {
1009 if matches!(op, BashArithOp::Div | BashArithOp::Mod) && *right_val == 0 {
1011 continue;
1012 }
1013
1014 let node = BashNode::Assignment {
1015 name: "result".to_string(),
1016 value: Box::new(BashNode::Arithmetic(Box::new(BashNode::ArithOp {
1017 left: Box::new(BashNode::IntLit(*left_val)),
1018 op: *op,
1019 right: Box::new(BashNode::IntLit(*right_val)),
1020 }))),
1021 };
1022 if node.depth() <= self.max_depth {
1023 programs.push(self.node_to_generated(&node));
1024 }
1025 }
1026 }
1027 }
1028
1029 if self.max_depth >= 2 {
1031 for var in &["i", "j", "x", "item", "file"] {
1032 for items in &[
1034 vec![BashNode::IntLit(1)],
1035 vec![BashNode::IntLit(1), BashNode::IntLit(2)],
1036 vec![
1037 BashNode::IntLit(1),
1038 BashNode::IntLit(2),
1039 BashNode::IntLit(3),
1040 ],
1041 vec![
1042 BashNode::IntLit(0),
1043 BashNode::IntLit(1),
1044 BashNode::IntLit(2),
1045 BashNode::IntLit(3),
1046 BashNode::IntLit(4),
1047 ],
1048 ] {
1049 let node = BashNode::For {
1050 var: var.to_string(),
1051 items: items.clone(),
1052 body: vec![BashNode::Command {
1053 name: "echo".to_string(),
1054 args: vec![BashNode::Variable(var.to_string())],
1055 }],
1056 };
1057 if node.depth() <= self.max_depth {
1058 programs.push(self.node_to_generated(&node));
1059 }
1060 }
1061
1062 for pattern in &["*.txt", "*.sh", "*.log", "*", "file*", "*.{txt,md}"] {
1064 let node = BashNode::For {
1065 var: var.to_string(),
1066 items: vec![BashNode::StringLit(pattern.to_string())],
1067 body: vec![BashNode::Command {
1068 name: "echo".to_string(),
1069 args: vec![BashNode::Variable(var.to_string())],
1070 }],
1071 };
1072 if node.depth() <= self.max_depth {
1073 programs.push(self.node_to_generated(&node));
1074 }
1075 }
1076 }
1077 }
1078
1079 if self.max_depth >= 2 {
1081 for var in &["x", "n", "count", "i"] {
1082 for limit in &[0, 1, 5, 10] {
1083 for op in &[
1084 BashCompareOp::NumGt,
1085 BashCompareOp::NumLt,
1086 BashCompareOp::NumGe,
1087 BashCompareOp::NumLe,
1088 ] {
1089 let node = BashNode::While {
1090 condition: Box::new(BashNode::Test {
1091 double: false,
1092 expr: Box::new(BashNode::Compare {
1093 left: Box::new(BashNode::Variable(var.to_string())),
1094 op: *op,
1095 right: Box::new(BashNode::IntLit(*limit)),
1096 }),
1097 }),
1098 body: vec![BashNode::Assignment {
1099 name: var.to_string(),
1100 value: Box::new(BashNode::Arithmetic(Box::new(
1101 BashNode::ArithOp {
1102 left: Box::new(BashNode::Variable(var.to_string())),
1103 op: BashArithOp::Sub,
1104 right: Box::new(BashNode::IntLit(1)),
1105 },
1106 ))),
1107 }],
1108 };
1109 if node.depth() <= self.max_depth {
1110 programs.push(self.node_to_generated(&node));
1111 }
1112 }
1113 }
1114 }
1115 }
1116
1117 if self.max_depth >= 2 {
1119 let func_names = [
1120 "main", "init", "setup", "cleanup", "run", "process", "validate", "check", "build",
1121 "deploy",
1122 ];
1123 let bodies = ["echo done", "return 0", "exit 0", "true", "pwd"];
1124
1125 for name in &func_names {
1126 for body_cmd in &bodies {
1127 let parts: Vec<&str> = body_cmd.split_whitespace().collect();
1128 let cmd_name = parts[0];
1129 let args: Vec<BashNode> = parts[1..]
1130 .iter()
1131 .map(|a| BashNode::StringLit(a.to_string()))
1132 .collect();
1133
1134 let node = BashNode::Function {
1135 name: name.to_string(),
1136 body: vec![BashNode::Command {
1137 name: cmd_name.to_string(),
1138 args,
1139 }],
1140 };
1141 if node.depth() <= self.max_depth {
1142 programs.push(self.node_to_generated(&node));
1143 }
1144 }
1145 }
1146 }
1147
1148 if self.max_depth >= 2 {
1150 for var in &["x", "opt", "arg", "cmd"] {
1151 for patterns in &[
1152 vec![("1", "one"), ("2", "two"), ("*", "other")],
1153 vec![("a", "alpha"), ("b", "beta"), ("*", "default")],
1154 vec![
1155 ("start", "starting"),
1156 ("stop", "stopping"),
1157 ("*", "unknown"),
1158 ],
1159 vec![("yes", "y"), ("no", "n"), ("*", "invalid")],
1160 ] {
1161 let pattern_nodes: Vec<(String, Vec<BashNode>)> = patterns
1162 .iter()
1163 .map(|(pat, resp)| {
1164 (
1165 pat.to_string(),
1166 vec![BashNode::Command {
1167 name: "echo".to_string(),
1168 args: vec![BashNode::StringLit(resp.to_string())],
1169 }],
1170 )
1171 })
1172 .collect();
1173
1174 let node = BashNode::Case {
1175 value: Box::new(BashNode::Variable(var.to_string())),
1176 patterns: pattern_nodes,
1177 };
1178 if node.depth() <= self.max_depth {
1179 programs.push(self.node_to_generated(&node));
1180 }
1181 }
1182 }
1183 }
1184
1185 if self.max_depth >= 2 {
1187 let pipe_lefts = [
1188 ("echo", "hello"),
1189 ("cat", "file.txt"),
1190 ("ls", "-la"),
1191 ("ps", "aux"),
1192 ];
1193 let pipe_rights = [
1194 ("grep", "pattern"),
1195 ("wc", "-l"),
1196 ("head", "-n10"),
1197 ("sort", "-n"),
1198 ("cut", "-d:"),
1199 ];
1200
1201 for (left_cmd, left_arg) in &pipe_lefts {
1202 for (right_cmd, right_arg) in &pipe_rights {
1203 let node = BashNode::Pipe {
1204 left: Box::new(BashNode::Command {
1205 name: left_cmd.to_string(),
1206 args: vec![BashNode::StringLit(left_arg.to_string())],
1207 }),
1208 right: Box::new(BashNode::Command {
1209 name: right_cmd.to_string(),
1210 args: vec![BashNode::StringLit(right_arg.to_string())],
1211 }),
1212 };
1213 if node.depth() <= self.max_depth {
1214 programs.push(self.node_to_generated(&node));
1215 }
1216 }
1217 }
1218 }
1219
1220 for redirect_type in &[
1222 RedirectType::Output,
1223 RedirectType::Append,
1224 RedirectType::Input,
1225 RedirectType::StderrToStdout,
1226 ] {
1227 for target in &[
1228 "/dev/null",
1229 "/tmp/out.txt",
1230 "output.log",
1231 "result.txt",
1232 "&1",
1233 "&2",
1234 ] {
1235 for cmd in &["echo", "cat", "ls"] {
1236 let node = BashNode::Redirect {
1237 command: Box::new(BashNode::Command {
1238 name: cmd.to_string(),
1239 args: vec![BashNode::StringLit("test".to_string())],
1240 }),
1241 redirect_type: *redirect_type,
1242 target: target.to_string(),
1243 };
1244 if node.depth() <= self.max_depth {
1245 programs.push(self.node_to_generated(&node));
1246 }
1247 }
1248 }
1249 }
1250
1251 for var in &["output", "result", "data", "lines", "count"] {
1253 for cmd in &["pwd", "date", "whoami", "hostname", "uname -a"] {
1254 let parts: Vec<&str> = cmd.split_whitespace().collect();
1255 let cmd_name = parts[0];
1256 let args: Vec<BashNode> = parts[1..]
1257 .iter()
1258 .map(|a| BashNode::StringLit(a.to_string()))
1259 .collect();
1260
1261 let node = BashNode::Assignment {
1262 name: var.to_string(),
1263 value: Box::new(BashNode::CommandSubst(Box::new(BashNode::Command {
1264 name: cmd_name.to_string(),
1265 args,
1266 }))),
1267 };
1268 if node.depth() <= self.max_depth {
1269 programs.push(self.node_to_generated(&node));
1270 }
1271 }
1272 }
1273
1274 for var in &["arr", "list", "items", "values", "data"] {
1276 for items in &[
1277 vec![BashNode::IntLit(1)],
1278 vec![BashNode::IntLit(1), BashNode::IntLit(2)],
1279 vec![
1280 BashNode::IntLit(1),
1281 BashNode::IntLit(2),
1282 BashNode::IntLit(3),
1283 ],
1284 vec![BashNode::StringLit("a".to_string())],
1285 vec![
1286 BashNode::StringLit("a".to_string()),
1287 BashNode::StringLit("b".to_string()),
1288 ],
1289 vec![
1290 BashNode::StringLit("foo".to_string()),
1291 BashNode::StringLit("bar".to_string()),
1292 BashNode::StringLit("baz".to_string()),
1293 ],
1294 ] {
1295 let node = BashNode::Assignment {
1296 name: var.to_string(),
1297 value: Box::new(BashNode::Array(items.clone())),
1298 };
1299 if node.depth() <= self.max_depth {
1300 programs.push(self.node_to_generated(&node));
1301 }
1302 }
1303 }
1304
1305 if self.max_depth >= 2 {
1307 for var in &["x", "n", "flag", "status"] {
1308 for op in &[
1309 BashCompareOp::NumEq,
1310 BashCompareOp::NumNe,
1311 BashCompareOp::NumGt,
1312 ] {
1313 for val in &[0, 1] {
1314 let node = BashNode::If {
1315 condition: Box::new(BashNode::Test {
1316 double: false,
1317 expr: Box::new(BashNode::Compare {
1318 left: Box::new(BashNode::Variable(var.to_string())),
1319 op: *op,
1320 right: Box::new(BashNode::IntLit(*val)),
1321 }),
1322 }),
1323 then_body: vec![BashNode::Command {
1324 name: "echo".to_string(),
1325 args: vec![BashNode::StringLit("then".to_string())],
1326 }],
1327 else_body: vec![BashNode::Command {
1328 name: "echo".to_string(),
1329 args: vec![BashNode::StringLit("else".to_string())],
1330 }],
1331 };
1332 if node.depth() <= self.max_depth {
1333 programs.push(self.node_to_generated(&node));
1334 }
1335 }
1336 }
1337 }
1338 }
1339
1340 for var in &["HOME", "USER", "PWD", "PATH", "SHELL", "TERM", "HOSTNAME"] {
1344 let node = BashNode::Command {
1345 name: "echo".to_string(),
1346 args: vec![BashNode::Variable(var.to_string())],
1347 };
1348 if node.depth() <= self.max_depth {
1349 programs.push(self.node_to_generated(&node));
1350 }
1351 }
1352
1353 for src in &["x", "y", "HOME", "USER"] {
1355 for dst in &["a", "b", "tmp"] {
1356 let node = BashNode::Assignment {
1357 name: dst.to_string(),
1358 value: Box::new(BashNode::Variable(src.to_string())),
1359 };
1360 if node.depth() <= self.max_depth {
1361 programs.push(self.node_to_generated(&node));
1362 }
1363 }
1364 }
1365
1366 for cmd in &["pwd", "date", "whoami", "hostname"] {
1368 let node = BashNode::Command {
1369 name: "echo".to_string(),
1370 args: vec![BashNode::CommandSubst(Box::new(BashNode::Command {
1371 name: cmd.to_string(),
1372 args: vec![],
1373 }))],
1374 };
1375 if node.depth() <= self.max_depth {
1376 programs.push(self.node_to_generated(&node));
1377 }
1378 }
1379
1380 if self.max_depth >= 2 {
1382 for var in &["str", "name", "arg"] {
1383 for op in &[BashCompareOp::StrEq, BashCompareOp::StrNe] {
1384 for value in &["", "test", "value", "hello"] {
1385 let node = BashNode::If {
1386 condition: Box::new(BashNode::Test {
1387 double: true,
1388 expr: Box::new(BashNode::Compare {
1389 left: Box::new(BashNode::Variable(var.to_string())),
1390 op: *op,
1391 right: Box::new(BashNode::StringLit(value.to_string())),
1392 }),
1393 }),
1394 then_body: vec![BashNode::Command {
1395 name: "echo".to_string(),
1396 args: vec![BashNode::StringLit("match".to_string())],
1397 }],
1398 else_body: vec![],
1399 };
1400 if node.depth() <= self.max_depth {
1401 programs.push(self.node_to_generated(&node));
1402 }
1403 }
1404 }
1405 }
1406 }
1407
1408 for var in &["x", "y", "n"] {
1410 for op in BashArithOp::all() {
1411 for val in &[1, 2, 5] {
1412 let node = BashNode::Assignment {
1413 name: "result".to_string(),
1414 value: Box::new(BashNode::Arithmetic(Box::new(BashNode::ArithOp {
1415 left: Box::new(BashNode::Variable(var.to_string())),
1416 op: *op,
1417 right: Box::new(BashNode::IntLit(*val)),
1418 }))),
1419 };
1420 if node.depth() <= self.max_depth {
1421 programs.push(self.node_to_generated(&node));
1422 }
1423 }
1424 }
1425 }
1426
1427 for cmd in &["echo", "printf", "ls", "cat"] {
1429 for arg1 in &["hello", "world", "-l", "-a"] {
1430 for arg2 in &["test", "file", "-n", "-r"] {
1431 let node = BashNode::Command {
1432 name: cmd.to_string(),
1433 args: vec![
1434 BashNode::StringLit(arg1.to_string()),
1435 BashNode::StringLit(arg2.to_string()),
1436 ],
1437 };
1438 if node.depth() <= self.max_depth {
1439 programs.push(self.node_to_generated(&node));
1440 }
1441 }
1442 }
1443 }
1444
1445 programs
1446 }
1447
1448 fn node_to_generated(&self, node: &BashNode) -> GeneratedCode {
1449 GeneratedCode {
1450 code: node.to_code(),
1451 language: Language::Bash,
1452 ast_depth: node.depth(),
1453 features: node.features(),
1454 }
1455 }
1456}
1457
1458#[cfg(test)]
1459mod tests {
1460 use super::*;
1461
1462 #[test]
1463 fn test_bash_node_assignment() {
1464 let node = BashNode::Assignment {
1465 name: "x".to_string(),
1466 value: Box::new(BashNode::IntLit(42)),
1467 };
1468 assert_eq!(node.to_code(), "x=42");
1469 }
1470
1471 #[test]
1472 fn test_bash_node_string_assignment() {
1473 let node = BashNode::Assignment {
1474 name: "msg".to_string(),
1475 value: Box::new(BashNode::StringLit("hello world".to_string())),
1476 };
1477 assert_eq!(node.to_code(), "msg=\"hello world\"");
1478 }
1479
1480 #[test]
1481 fn test_bash_node_echo() {
1482 let node = BashNode::Command {
1483 name: "echo".to_string(),
1484 args: vec![BashNode::StringLit("hello".to_string())],
1485 };
1486 assert_eq!(node.to_code(), "echo hello");
1487 }
1488
1489 #[test]
1490 fn test_bash_node_if() {
1491 let node = BashNode::If {
1492 condition: Box::new(BashNode::Test {
1493 double: false,
1494 expr: Box::new(BashNode::Compare {
1495 left: Box::new(BashNode::Variable("x".to_string())),
1496 op: BashCompareOp::NumEq,
1497 right: Box::new(BashNode::IntLit(1)),
1498 }),
1499 }),
1500 then_body: vec![BashNode::Command {
1501 name: "echo".to_string(),
1502 args: vec![BashNode::StringLit("yes".to_string())],
1503 }],
1504 else_body: vec![],
1505 };
1506 let code = node.to_code();
1507 assert!(code.contains("if [ $x -eq 1 ]"));
1508 assert!(code.contains("then"));
1509 assert!(code.contains("fi"));
1510 }
1511
1512 #[test]
1513 fn test_bash_node_for() {
1514 let node = BashNode::For {
1515 var: "i".to_string(),
1516 items: vec![BashNode::IntLit(1), BashNode::IntLit(2)],
1517 body: vec![BashNode::Command {
1518 name: "echo".to_string(),
1519 args: vec![BashNode::Variable("i".to_string())],
1520 }],
1521 };
1522 let code = node.to_code();
1523 assert!(code.contains("for i in 1 2"));
1524 assert!(code.contains("do"));
1525 assert!(code.contains("done"));
1526 }
1527
1528 #[test]
1529 fn test_bash_node_while() {
1530 let node = BashNode::While {
1531 condition: Box::new(BashNode::Test {
1532 double: true,
1533 expr: Box::new(BashNode::Compare {
1534 left: Box::new(BashNode::Variable("x".to_string())),
1535 op: BashCompareOp::NumGt,
1536 right: Box::new(BashNode::IntLit(0)),
1537 }),
1538 }),
1539 body: vec![BashNode::Command {
1540 name: "echo".to_string(),
1541 args: vec![BashNode::Variable("x".to_string())],
1542 }],
1543 };
1544 let code = node.to_code();
1545 assert!(code.contains("while [[ $x -gt 0 ]]"));
1546 assert!(code.contains("done"));
1547 }
1548
1549 #[test]
1550 fn test_bash_node_function() {
1551 let node = BashNode::Function {
1552 name: "greet".to_string(),
1553 body: vec![BashNode::Command {
1554 name: "echo".to_string(),
1555 args: vec![BashNode::StringLit("Hello".to_string())],
1556 }],
1557 };
1558 let code = node.to_code();
1559 assert!(code.contains("greet()"));
1560 assert!(code.contains("{"));
1561 assert!(code.contains("}"));
1562 }
1563
1564 #[test]
1565 fn test_bash_node_arithmetic() {
1566 let node = BashNode::Arithmetic(Box::new(BashNode::ArithOp {
1567 left: Box::new(BashNode::IntLit(1)),
1568 op: BashArithOp::Add,
1569 right: Box::new(BashNode::IntLit(2)),
1570 }));
1571 assert_eq!(node.to_code(), "$((1 + 2))");
1572 }
1573
1574 #[test]
1575 fn test_bash_node_pipe() {
1576 let node = BashNode::Pipe {
1577 left: Box::new(BashNode::Command {
1578 name: "ls".to_string(),
1579 args: vec![],
1580 }),
1581 right: Box::new(BashNode::Command {
1582 name: "wc".to_string(),
1583 args: vec![BashNode::StringLit("-l".to_string())],
1584 }),
1585 };
1586 assert_eq!(node.to_code(), "ls | wc -l");
1587 }
1588
1589 #[test]
1590 fn test_bash_node_depth() {
1591 let simple = BashNode::IntLit(1);
1592 assert_eq!(simple.depth(), 1);
1593
1594 let assign = BashNode::Assignment {
1595 name: "x".to_string(),
1596 value: Box::new(BashNode::IntLit(1)),
1597 };
1598 assert_eq!(assign.depth(), 2);
1599 }
1600
1601 #[test]
1602 fn test_bash_compare_op_all() {
1603 let ops = BashCompareOp::all();
1604 assert!(ops.len() >= 6);
1605 }
1606
1607 #[test]
1608 fn test_bash_arith_op_all() {
1609 let ops = BashArithOp::all();
1610 assert_eq!(ops.len(), 5);
1611 }
1612
1613 #[test]
1614 fn test_bash_enumerator_generates_programs() {
1615 let enumerator = BashEnumerator::new(3);
1616 let programs = enumerator.enumerate_programs();
1617 assert!(!programs.is_empty());
1618
1619 for prog in &programs {
1620 assert_eq!(prog.language, Language::Bash);
1621 assert!(prog.ast_depth <= 3);
1622 }
1623 }
1624
1625 #[test]
1626 fn test_bash_enumerator_depth_1() {
1627 let enumerator = BashEnumerator::new(1);
1628 let programs = enumerator.enumerate_programs();
1629
1630 for prog in &programs {
1631 assert!(prog.ast_depth <= 1);
1632 }
1633 }
1634
1635 #[test]
1636 fn test_bash_enumerator_depth_2() {
1637 let enumerator = BashEnumerator::new(2);
1638 let programs = enumerator.enumerate_programs();
1639
1640 let depth1_count = BashEnumerator::new(1).enumerate_programs().len();
1642 assert!(programs.len() > depth1_count);
1643 }
1644
1645 #[test]
1646 fn test_bash_node_features() {
1647 let node = BashNode::If {
1648 condition: Box::new(BashNode::Test {
1649 double: false,
1650 expr: Box::new(BashNode::Variable("x".to_string())),
1651 }),
1652 then_body: vec![],
1653 else_body: vec![],
1654 };
1655 let features = node.features();
1656 assert!(features.contains(&"if".to_string()));
1657 }
1658}
1659
1660#[test]
1661fn test_bash_program_count() {
1662 let enumerator = BashEnumerator::new(3);
1663 let programs = enumerator.enumerate_programs();
1664 println!("Generated {} bash programs at depth 3", programs.len());
1665 assert!(
1666 programs.len() >= 1000,
1667 "Expected at least 1000 programs, got {}",
1668 programs.len()
1669 );
1670}
1671
1672#[test]
1673fn test_bash_program_uniqueness() {
1674 use std::collections::HashSet;
1675
1676 let enumerator = BashEnumerator::new(3);
1677 let programs = enumerator.enumerate_programs();
1678
1679 let unique: HashSet<_> = programs.iter().map(|p| &p.code).collect();
1680 let unique_count = unique.len();
1681 let total_count = programs.len();
1682
1683 println!(
1684 "Unique: {}/{} ({:.1}%)",
1685 unique_count,
1686 total_count,
1687 100.0 * unique_count as f64 / total_count as f64
1688 );
1689
1690 assert!(
1692 unique_count as f64 / total_count as f64 >= 0.80,
1693 "Expected at least 80% unique programs, got {}%",
1694 100 * unique_count / total_count
1695 );
1696}
1697
1698#[test]
1699fn test_bash_feature_coverage() {
1700 use std::collections::HashSet;
1701
1702 let enumerator = BashEnumerator::new(5);
1704 let programs = enumerator.enumerate_programs();
1705
1706 let mut all_features = HashSet::new();
1707 let mut all_code = String::new();
1708
1709 for prog in &programs {
1710 for feature in &prog.features {
1711 all_features.insert(feature.clone());
1712 }
1713 all_code.push_str(&prog.code);
1714 all_code.push('\n');
1715 }
1716
1717 println!("Covered features: {:?}", all_features);
1718
1719 let required_features = ["assignment", "command", "for", "function", "pipe", "case"];
1721 for feature in required_features {
1722 assert!(
1723 all_features.iter().any(|f| f.contains(feature)),
1724 "Missing feature: {feature}"
1725 );
1726 }
1727
1728 let has_if = all_code.contains("if [") || all_code.contains("if;");
1731 let has_while = all_code.contains("while [") || all_code.contains("while;");
1732
1733 let has_if_feature = all_features.contains("if");
1735 let has_while_feature = all_features.contains("while");
1736
1737 assert!(has_if || has_if_feature, "Missing if statements");
1738 assert!(has_while || has_while_feature, "Missing while loops");
1739}
1740
1741#[test]
1742fn test_bash_depth_distribution() {
1743 let enumerator = BashEnumerator::new(3);
1744 let programs = enumerator.enumerate_programs();
1745
1746 let mut depth_counts = [0usize; 4];
1747 for prog in &programs {
1748 if prog.ast_depth <= 3 {
1749 depth_counts[prog.ast_depth] += 1;
1750 }
1751 }
1752
1753 println!("Depth distribution: {:?}", depth_counts);
1754
1755 assert!(depth_counts[1] > 0, "No depth-1 programs");
1757 assert!(depth_counts[2] > 0, "No depth-2 programs");
1758}