use super::*;
use crate::linter::{Diagnostic, Fix, Severity};
#[test]
fn test_apply_single_fix_basic() {
let source = "echo $VAR\n";
let span = Span::new(1, 6, 1, 10); let replacement = "\"$VAR\"";
let result = apply_single_fix(source, &span, replacement).unwrap();
assert_eq!(result, "echo \"$VAR\"\n");
}
#[test]
fn test_apply_multiple_fixes_reverse_order() {
let source = "ls $DIR1 $DIR2\n";
let mut result = LintResult::new();
result.add(
Diagnostic::new(
"SC2086",
Severity::Warning,
"Unquoted $DIR1".to_string(),
Span::new(1, 4, 1, 9),
)
.with_fix(Fix::new("\"$DIR1\"".to_string())),
);
result.add(
Diagnostic::new(
"SC2086",
Severity::Warning,
"Unquoted $DIR2".to_string(),
Span::new(1, 10, 1, 15),
)
.with_fix(Fix::new("\"$DIR2\"".to_string())),
);
let options = FixOptions::default();
let fix_result = apply_fixes(source, &result, &options).unwrap();
assert_eq!(fix_result.fixes_applied, 2);
assert_eq!(
fix_result.modified_source.unwrap(),
"ls \"$DIR1\" \"$DIR2\"\n"
);
}
#[test]
fn test_dry_run_mode() {
let source = "echo $VAR\n";
let mut result = LintResult::new();
result.add(
Diagnostic::new(
"SC2086",
Severity::Warning,
"Unquoted".to_string(),
Span::new(1, 6, 1, 10),
)
.with_fix(Fix::new("\"$VAR\"".to_string())),
);
let options = FixOptions {
dry_run: true,
..Default::default()
};
let fix_result = apply_fixes(source, &result, &options).unwrap();
assert_eq!(fix_result.fixes_applied, 1);
assert!(fix_result.modified_source.is_none()); }
#[test]
fn test_no_fixes_to_apply() {
let source = "echo \"$VAR\"\n";
let result = LintResult::new();
let options = FixOptions::default();
let fix_result = apply_fixes(source, &result, &options).unwrap();
assert_eq!(fix_result.fixes_applied, 0);
assert_eq!(fix_result.modified_source.unwrap(), source);
}
#[test]
fn test_invalid_span() {
let source = "echo test\n";
let span = Span::new(999, 1, 999, 5);
let result = apply_single_fix(source, &span, "replacement");
assert!(result.is_err());
}
#[test]
fn test_conflicting_fixes_priority() {
let source = "RELEASE=$(echo $TIMESTAMP)\n";
let mut result = LintResult::new();
result.add(
Diagnostic::new(
"SC2116",
Severity::Warning,
"Useless echo".to_string(),
Span::new(1, 9, 1, 27), )
.with_fix(Fix::new("$TIMESTAMP".to_string())),
);
result.add(
Diagnostic::new(
"SC2046",
Severity::Warning,
"Unquoted command substitution".to_string(),
Span::new(1, 9, 1, 27), )
.with_fix(Fix::new("\"$(echo $TIMESTAMP)\"".to_string())),
);
let options = FixOptions::default();
let fix_result = apply_fixes(source, &result, &options).unwrap();
assert_eq!(fix_result.fixes_applied, 1);
assert_eq!(fix_result.modified_source.unwrap(), "RELEASE=$TIMESTAMP\n");
}
#[test]
fn test_non_overlapping_fixes() {
let source = "cp $FILE1 $FILE2\n";
let mut result = LintResult::new();
result.add(
Diagnostic::new(
"SC2086",
Severity::Warning,
"Unquoted $FILE1".to_string(),
Span::new(1, 4, 1, 10),
)
.with_fix(Fix::new("\"$FILE1\"".to_string())),
);
result.add(
Diagnostic::new(
"SC2086",
Severity::Warning,
"Unquoted $FILE2".to_string(),
Span::new(1, 11, 1, 17),
)
.with_fix(Fix::new("\"$FILE2\"".to_string())),
);
let options = FixOptions::default();
let fix_result = apply_fixes(source, &result, &options).unwrap();
assert_eq!(fix_result.fixes_applied, 2);
assert_eq!(
fix_result.modified_source.unwrap(),
"cp \"$FILE1\" \"$FILE2\"\n"
);
}
#[test]
fn test_overlap_detection() {
let span_a = Span::new(1, 5, 1, 10);
let span_b = Span::new(1, 8, 1, 12);
assert!(spans_overlap(&span_a, &span_b));
assert!(spans_overlap(&span_b, &span_a));
let span_c = Span::new(1, 11, 1, 15); assert!(!spans_overlap(&span_a, &span_c));
let span_d = Span::new(2, 5, 2, 10); assert!(!spans_overlap(&span_a, &span_d));
}
#[test]
fn test_backup_created_only_when_both_flags_true() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "echo $VAR").unwrap();
let temp_path = temp_file.path();
let mut result = LintResult::new();
result.add(
Diagnostic::new(
"SC2086",
Severity::Warning,
"Unquoted".to_string(),
Span::new(1, 6, 1, 10),
)
.with_fix(Fix::new("\"$VAR\"".to_string())),
);
let options = FixOptions {
create_backup: true,
dry_run: false,
backup_suffix: ".bak".to_string(),
apply_assumptions: false,
output_path: None,
};
let fix_result = apply_fixes_to_file(temp_path, &result, &options).unwrap();
assert!(
fix_result.backup_path.is_some(),
"Backup should be created when create_backup=true AND dry_run=false"
);
if let Some(backup) = fix_result.backup_path {
let _ = std::fs::remove_file(backup);
}
let options_no_backup = FixOptions {
create_backup: false, dry_run: false,
backup_suffix: ".bak".to_string(),
apply_assumptions: false,
output_path: None,
};
let mut temp_file2 = NamedTempFile::new().unwrap();
writeln!(temp_file2, "echo $VAR").unwrap();
let temp_path2 = temp_file2.path();
let fix_result2 = apply_fixes_to_file(temp_path2, &result, &options_no_backup).unwrap();
assert!(
fix_result2.backup_path.is_none(),
"Backup should NOT be created when create_backup=false"
);
let options_dry_run = FixOptions {
create_backup: true,
dry_run: true, backup_suffix: ".bak".to_string(),
apply_assumptions: false,
output_path: None,
};
let mut temp_file3 = NamedTempFile::new().unwrap();
writeln!(temp_file3, "echo $VAR").unwrap();
let temp_path3 = temp_file3.path();
let fix_result3 = apply_fixes_to_file(temp_path3, &result, &options_dry_run).unwrap();
assert!(
fix_result3.backup_path.is_none(),
"Backup should NOT be created when dry_run=true"
);
}
#[test]
fn test_fix_priority_sc2046_coverage() {
let source = "cp $(cat file.txt) /dest\n";
let mut result = LintResult::new();
result.add(
Diagnostic::new(
"SC2046",
Severity::Warning,
"Unquoted command substitution".to_string(),
Span::new(1, 4, 1, 22),
)
.with_fix(Fix::new("\"$(cat file.txt)\"".to_string())),
);
let options = FixOptions::default();
let fix_result = apply_fixes(source, &result, &options).unwrap();
assert_eq!(fix_result.fixes_applied, 1);
assert!(fix_result.modified_source.is_some());
assert!(fix_result
.modified_source
.unwrap()
.contains("\"$(cat file.txt)\""));
}
#[test]
fn test_span_boundary_conditions() {
let source = "$VAR rest\n";
let span = Span::new(1, 1, 1, 5); let result = apply_single_fix(source, &span, "\"$VAR\"");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "\"$VAR\" rest\n");
let source = "start $VAR\n";
let span = Span::new(1, 7, 1, 11); let result = apply_single_fix(source, &span, "\"$VAR\"");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "start \"$VAR\"\n");
let source = "line1\necho $VAR\nline3\n";
let span = Span::new(2, 6, 2, 10); let result = apply_single_fix(source, &span, "\"$VAR\"");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "line1\necho \"$VAR\"\nline3\n");
}
#[test]
fn test_logical_operators_in_conditions() {
let source = "echo test\n";
let valid_span = Span::new(1, 6, 1, 10); let result = apply_single_fix(source, &valid_span, "replacement");
assert!(result.is_ok(), "Both conditions true should succeed");
let invalid_span = Span::new(1, 6, 1, 999); let result = apply_single_fix(source, &invalid_span, "replacement");
assert!(result.is_err(), "Invalid end_idx should fail");
let invalid_span2 = Span::new(999, 1, 999, 5); let result = apply_single_fix(source, &invalid_span2, "replacement");
assert!(result.is_err(), "Invalid start line should fail");
}