#![allow(clippy::unwrap_used)]
use crate::repl::debugger::*;
#[test]
fn test_REPL_010_003_explain_ln_sf() {
let script = "ln -s /tmp/src /tmp/link";
let session = DebugSession::new(script);
let explanation = session.explain_current_line();
if let Some(text) = explanation {
assert!(
text.contains("-f") || text.contains("idempot") || text.contains("idem"),
"Explanation should mention -f or idempotency: {}",
text
);
}
}
#[test]
fn test_REPL_010_003_explain_no_change() {
let script = "mkdir -p /tmp/foo";
let session = DebugSession::new(script);
let explanation = session.explain_current_line();
assert!(
explanation.is_none() || !explanation.as_ref().unwrap().is_empty(),
"Should produce valid output, got: {:?}",
explanation
);
}
#[test]
fn test_REPL_010_003_explain_multiple_changes() {
let script = "rm $FILE";
let session = DebugSession::new(script);
let explanation = session.explain_current_line();
if let Some(text) = explanation {
assert!(
text.contains("quot") || text.contains("-f"),
"Should explain at least one transformation: {}",
text
);
}
}
#[cfg(test)]
mod property_tests {
use crate::repl::debugger::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_step_never_skips_lines(num_lines in 1usize..20) {
let script = (0..num_lines)
.map(|i| format!("echo line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session = DebugSession::new(&script);
for expected_line in 1..=num_lines {
prop_assert_eq!(
session.current_line(),
expected_line,
"Should be at line {} before step",
expected_line
);
if expected_line < num_lines {
prop_assert!(!session.is_finished(), "Should not be finished");
}
session.step();
}
prop_assert!(session.is_finished(), "Should be finished after all lines");
}
}
proptest! {
#[test]
fn prop_current_line_always_valid(num_lines in 1usize..20, steps in 0usize..25) {
let script = (0..num_lines)
.map(|i| format!("line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session = DebugSession::new(&script);
for _ in 0..steps {
let line = session.current_line();
prop_assert!(line >= 1, "Line number should be >= 1");
prop_assert!(line <= num_lines + 1, "Line number should be reasonable");
if session.is_finished() {
break;
}
session.step();
}
}
}
proptest! {
#[test]
fn prop_total_lines_constant(num_lines in 1usize..20, steps in 0usize..25) {
let script = (0..num_lines)
.map(|i| format!("line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session = DebugSession::new(&script);
let initial_total = session.total_lines();
for _ in 0..steps {
prop_assert_eq!(
session.total_lines(),
initial_total,
"Total lines should never change"
);
if session.is_finished() {
break;
}
session.step();
}
}
}
proptest! {
#[test]
fn prop_next_never_goes_deeper(num_lines in 1usize..20) {
let script = (0..num_lines)
.map(|i| format!("echo line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session = DebugSession::new(&script);
let initial_depth = session.call_depth();
for _ in 0..num_lines {
if session.is_finished() {
break;
}
let depth_before = session.call_depth();
session.step_over();
let depth_after = session.call_depth();
prop_assert!(
depth_after <= depth_before,
"next() should never increase call depth (was {}, now {})",
depth_before,
depth_after
);
prop_assert!(
depth_after <= initial_depth,
"Call depth should never exceed initial depth"
);
}
}
}
proptest! {
#[test]
fn prop_next_eventually_finishes(num_lines in 1usize..100) {
let script = (0..num_lines)
.map(|i| format!("echo line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session = DebugSession::new(&script);
let max_iterations = num_lines * 2;
for _i in 0..max_iterations {
if session.is_finished() {
return Ok(());
}
session.step_over();
}
prop_assert!(
session.is_finished(),
"Session should finish after {} next() calls on {} line script",
max_iterations,
num_lines
);
}
}
proptest! {
#[test]
fn prop_continue_no_breakpoints_finishes(num_lines in 1usize..20) {
let script = (0..num_lines)
.map(|i| format!("echo line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session = DebugSession::new(&script);
let result = session.continue_execution();
prop_assert_eq!(result, ContinueResult::Finished, "Should finish without breakpoints");
prop_assert!(session.is_finished(), "Session should be finished");
}
}
proptest! {
#[test]
fn prop_continue_stops_at_breakpoint(
num_lines in 2usize..20,
breakpoint_line in 1usize..19
) {
let script = (0..num_lines)
.map(|i| format!("echo line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session = DebugSession::new(&script);
if breakpoint_line <= num_lines {
session.set_breakpoint(breakpoint_line);
let result = session.continue_execution();
match result {
ContinueResult::BreakpointHit(line) => {
prop_assert_eq!(line, breakpoint_line, "Should stop at correct breakpoint");
}
ContinueResult::Finished => {
prop_assert!(false, "Should not finish if breakpoint exists");
}
}
}
}
}
proptest! {
#[test]
fn prop_continue_deterministic(
num_lines in 1usize..20,
has_breakpoint in proptest::bool::ANY,
breakpoint_line in 1usize..19
) {
let script = (0..num_lines)
.map(|i| format!("echo line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session1 = DebugSession::new(&script);
let mut session2 = DebugSession::new(&script);
if has_breakpoint && breakpoint_line <= num_lines {
session1.set_breakpoint(breakpoint_line);
session2.set_breakpoint(breakpoint_line);
}
let result1 = session1.continue_execution();
let result2 = session2.continue_execution();
prop_assert_eq!(result1, result2, "Same setup should produce same result");
}
}
proptest! {
#[test]
fn prop_multiple_continues_finish(num_lines in 1usize..10) {
let script = (0..num_lines)
.map(|i| format!("echo line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session = DebugSession::new(&script);
for line in 1..=num_lines {
session.set_breakpoint(line);
}
let mut iterations = 0;
let max_iterations = num_lines + 5;
loop {
let result = session.continue_execution();
match result {
ContinueResult::Finished => break,
ContinueResult::BreakpointHit(_) => {
session.step();
}
}
iterations += 1;
if iterations > max_iterations {
prop_assert!(false, "Too many iterations, should have finished");
break;
}
}
prop_assert!(session.is_finished(), "Should eventually finish");
}
}
proptest! {
#[test]
fn prop_variable_set_get_matches(
var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,10}",
var_value in ".*{0,50}"
) {
let script = "echo test";
let mut session = DebugSession::new(script);
session.set_variable(&var_name, &var_value);
prop_assert_eq!(session.get_variable(&var_name), Some(var_value.as_str()));
}
}
proptest! {
#[test]
fn prop_variable_count_correct(num_vars in 0usize..20) {
let script = "echo test";
let mut session = DebugSession::new(script);
for i in 0..num_vars {
session.set_variable(format!("VAR{}", i), format!("value{}", i));
}
prop_assert_eq!(session.variable_count(), num_vars);
}
}
proptest! {
#[test]
fn prop_list_variables_complete(num_vars in 1usize..10) {
let script = "echo test";
let mut session = DebugSession::new(script);
for i in 0..num_vars {
session.set_variable(format!("VAR{}", i), format!("value{}", i));
}
let vars = session.list_variables();
prop_assert_eq!(vars.len(), num_vars, "List should contain all variables");
for i in 0..num_vars {
let name = format!("VAR{}", i);
let found = vars.iter().any(|(n, _)| *n == name);
prop_assert!(found, "Variable {} should be in list", name);
}
}
}
proptest! {
#[test]
fn prop_clear_removes_all(num_vars in 1usize..20) {
let script = "echo test";
let mut session = DebugSession::new(script);
for i in 0..num_vars {
session.set_variable(format!("VAR{}", i), format!("value{}", i));
}
prop_assert_eq!(session.variable_count(), num_vars);
session.clear_variables();
prop_assert_eq!(session.variable_count(), 0, "Count should be 0 after clear");
prop_assert_eq!(session.list_variables().len(), 0, "List should be empty after clear");
}
}
proptest! {
#[test]
fn prop_variables_persist_execution(
num_lines in 1usize..10,
num_vars in 1usize..5
) {
let script = (0..num_lines)
.map(|i| format!("echo line{}", i))
.collect::<Vec<_>>()
.join("\n");
let mut session = DebugSession::new(&script);
for i in 0..num_vars {
session.set_variable(format!("VAR{}", i), format!("value{}", i));
}
while !session.is_finished() {
session.step();
}
prop_assert_eq!(session.variable_count(), num_vars, "Variables should persist");
for i in 0..num_vars {
let name = format!("VAR{}", i);
let value = format!("value{}", i);
prop_assert_eq!(session.get_variable(&name), Some(value.as_str()));
}
}
}
proptest! {
#[test]
fn prop_get_env_deterministic(
var_name in "[A-Z_][A-Z0-9_]{0,20}"
) {
let script = "echo test";
let session = DebugSession::new(script);
let first = session.get_env(&var_name);
let second = session.get_env(&var_name);
prop_assert_eq!(first, second, "get_env should be deterministic");
}