use super::core::TestGenResult;
use super::coverage::UncoveredPath;
use crate::bash_parser::ast::*;
pub struct UnitTestGenerator;
impl Default for UnitTestGenerator {
fn default() -> Self {
Self::new()
}
}
impl UnitTestGenerator {
pub fn new() -> Self {
Self
}
pub fn generate_tests(&self, ast: &BashAst) -> TestGenResult<Vec<UnitTest>> {
let mut tests = Vec::new();
for stmt in &ast.statements {
if let BashStmt::Function { name, body, .. } = stmt {
tests.extend(self.generate_function_tests(name, body)?);
}
}
tests.extend(self.generate_edge_case_tests(ast)?);
tests.extend(self.generate_error_case_tests(ast)?);
Ok(tests)
}
fn generate_function_tests(
&self,
name: &str,
body: &[BashStmt],
) -> TestGenResult<Vec<UnitTest>> {
let mut tests = Vec::new();
tests.extend(self.generate_branch_tests(name, body)?);
tests.extend(self.generate_boundary_tests(name, body)?);
Ok(tests)
}
fn generate_branch_tests(&self, name: &str, body: &[BashStmt]) -> TestGenResult<Vec<UnitTest>> {
let mut tests = Vec::new();
for stmt in body {
match stmt {
BashStmt::If {
elif_blocks,
else_block,
..
} => {
tests.extend(self.generate_if_branch_tests(
name,
elif_blocks,
else_block.as_ref(),
));
}
BashStmt::While { .. } => {
tests.push(self.make_while_test(name));
}
BashStmt::For { .. } => {
tests.push(self.make_for_test(name));
}
_ => {}
}
}
Ok(tests)
}
fn generate_if_branch_tests(
&self,
name: &str,
elif_blocks: &[(BashExpr, Vec<BashStmt>)],
else_block: Option<&Vec<BashStmt>>,
) -> Vec<UnitTest> {
let mut tests = Vec::new();
tests.push(UnitTest {
name: format!("test_{}_if_then_branch", name),
test_fn: format!("{}()", name),
assertions: vec![Assertion::Comment("Test if-then branch".to_string())],
});
for (i, _) in elif_blocks.iter().enumerate() {
tests.push(UnitTest {
name: format!("test_{}_elif_{}_branch", name, i),
test_fn: format!("{}()", name),
assertions: vec![Assertion::Comment(format!("Test elif {} branch", i))],
});
}
if else_block.is_some() {
tests.push(UnitTest {
name: format!("test_{}_else_branch", name),
test_fn: format!("{}()", name),
assertions: vec![Assertion::Comment("Test else branch".to_string())],
});
}
tests
}
fn make_while_test(&self, name: &str) -> UnitTest {
UnitTest {
name: format!("test_{}_while_loop", name),
test_fn: format!("{}()", name),
assertions: vec![Assertion::Comment("Test while loop execution".to_string())],
}
}
fn make_for_test(&self, name: &str) -> UnitTest {
UnitTest {
name: format!("test_{}_for_loop", name),
test_fn: format!("{}()", name),
assertions: vec![Assertion::Comment("Test for loop iteration".to_string())],
}
}
fn generate_boundary_tests(
&self,
name: &str,
_body: &[BashStmt],
) -> TestGenResult<Vec<UnitTest>> {
vec![
UnitTest {
name: format!("test_{}_boundary_zero", name),
test_fn: format!("{}(0)", name),
assertions: vec![Assertion::Comment("Test with zero value".to_string())],
},
UnitTest {
name: format!("test_{}_boundary_one", name),
test_fn: format!("{}(1)", name),
assertions: vec![Assertion::Comment("Test with one value".to_string())],
},
]
.into_iter()
.map(Ok)
.collect()
}
fn generate_edge_case_tests(&self, ast: &BashAst) -> TestGenResult<Vec<UnitTest>> {
let mut tests = Vec::new();
for stmt in &ast.statements {
if let BashStmt::Function { name, .. } = stmt {
tests.push(UnitTest {
name: format!("test_{}_edge_case_empty_string", name),
test_fn: format!("{}(\"\")", name),
assertions: vec![Assertion::Comment(
"Test with empty string input".to_string(),
)],
});
tests.push(UnitTest {
name: format!("test_{}_edge_case_negative", name),
test_fn: format!("{}(-1)", name),
assertions: vec![Assertion::Comment("Test with negative value".to_string())],
});
tests.push(UnitTest {
name: format!("test_{}_edge_case_large_value", name),
test_fn: format!("{}(i64::MAX)", name),
assertions: vec![Assertion::Comment("Test with maximum value".to_string())],
});
}
}
Ok(tests)
}
fn generate_error_case_tests(&self, ast: &BashAst) -> TestGenResult<Vec<UnitTest>> {
let mut tests = Vec::new();
for stmt in &ast.statements {
if let BashStmt::Function { name, body, .. } = stmt {
if self.uses_file_operations(body) {
tests.push(UnitTest {
name: format!("test_{}_error_file_not_found", name),
test_fn: format!("{}(\"/nonexistent/file\")", name),
assertions: vec![Assertion::ShouldPanic {
expected_message: Some("File not found".to_string()),
}],
});
}
if self.uses_arithmetic(body) {
tests.push(UnitTest {
name: format!("test_{}_error_invalid_input", name),
test_fn: format!("{}(\"invalid\")", name),
assertions: vec![Assertion::ShouldPanic {
expected_message: Some("Invalid input".to_string()),
}],
});
}
}
}
Ok(tests)
}
pub fn generate_targeted_tests(
&self,
uncovered: &[UncoveredPath],
) -> TestGenResult<Vec<UnitTest>> {
let mut tests = Vec::new();
for path in uncovered {
match path {
UncoveredPath::Line(line) => {
tests.push(UnitTest {
name: format!("test_coverage_line_{}", line),
test_fn: "target_line()".to_string(),
assertions: vec![Assertion::Comment(format!("Cover line {}", line))],
});
}
UncoveredPath::Branch(branch) => {
tests.push(UnitTest {
name: format!("test_coverage_branch_{}", branch.function),
test_fn: format!("{}()", branch.function),
assertions: vec![Assertion::Comment(format!(
"Cover branch {:?}",
branch.branch_type
))],
});
}
UncoveredPath::Function(func) => {
tests.push(UnitTest {
name: format!("test_coverage_function_{}", func),
test_fn: format!("{}()", func),
assertions: vec![Assertion::Comment(format!("Cover function {}", func))],
});
}
}
}
Ok(tests)
}
fn uses_file_operations(&self, body: &[BashStmt]) -> bool {
for stmt in body {
if let BashStmt::Command { name, .. } = stmt {
if matches!(name.as_str(), "cat" | "ls" | "mkdir" | "rm" | "cp" | "mv") {
return true;
}
}
}
false
}
fn uses_arithmetic(&self, body: &[BashStmt]) -> bool {
for stmt in body {
if let BashStmt::Assignment { value, .. } = stmt {
if matches!(value, BashExpr::Arithmetic(_)) {
return true;
}
}
}
false
}
}
#[derive(Debug, Clone)]
pub struct UnitTest {
pub name: String,
pub test_fn: String,
pub assertions: Vec<Assertion>,
}
impl UnitTest {
pub fn to_rust_code(&self) -> String {
let mut code = "#[test]\n".to_string();
for assertion in &self.assertions {
if let Assertion::ShouldPanic { .. } = assertion {
if let Assertion::ShouldPanic {
expected_message: Some(msg),
} = assertion
{
code.push_str(&format!("#[should_panic(expected = \"{}\")]\n", msg));
} else {
code.push_str("#[should_panic]\n");
}
break;
}
}
code.push_str(&format!("fn {}() {{\n", self.name));
for assertion in &self.assertions {
code.push_str(&format!(" {}\n", assertion.to_rust_code()));
}
code.push_str("}\n");
code
}
}
#[derive(Debug, Clone)]
pub enum Assertion {
Equals { actual: String, expected: String },
NotEquals { actual: String, expected: String },
True { condition: String },
False { condition: String },
ShouldPanic { expected_message: Option<String> },
Comment(String),
}
impl Assertion {
fn to_rust_code(&self) -> String {
match self {
Assertion::Equals { actual, expected } => {
format!("assert_eq!({}, {});", actual, expected)
}
Assertion::NotEquals { actual, expected } => {
format!("assert_ne!({}, {});", actual, expected)
}
Assertion::True { condition } => {
format!("assert!({});", condition)
}
Assertion::False { condition } => {
format!("assert!(!{});", condition)
}
Assertion::ShouldPanic { .. } => {
"// Should panic test".to_string()
}
Assertion::Comment(text) => {
format!("// {}", text)
}
}
}
}