command arguments are now underlined if they match an existing path -m ran rustfmt on the entire codebase

This commit is contained in:
2026-02-19 21:32:03 -05:00
parent b668dab522
commit a18a0b622f
44 changed files with 5549 additions and 5019 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use std::collections::HashSet;
use crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB};
use crate::expand::{DUB_QUOTE, VAR_SUB, perform_param_expansion};
use crate::state::VarFlags;
use super::*;
@@ -293,70 +293,78 @@ fn param_expansion_replacesuffix() {
#[test]
fn dquote_escape_dollar() {
// "\$foo" should strip backslash, produce literal $foo (no expansion)
let result = unescape_str(r#""\$foo""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not become VAR_SUB");
assert!(result.contains('$'), "Literal $ should be preserved");
assert!(!result.contains('\\'), "Backslash should be stripped");
// "\$foo" should strip backslash, produce literal $foo (no expansion)
let result = unescape_str(r#""\$foo""#);
assert!(
!result.contains(VAR_SUB),
"Escaped $ should not become VAR_SUB"
);
assert!(result.contains('$'), "Literal $ should be preserved");
assert!(!result.contains('\\'), "Backslash should be stripped");
}
#[test]
fn dquote_escape_backslash() {
// "\\" in double quotes should produce a single backslash
let result = unescape_str(r#""\\""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert_eq!(inner, "\\", "Double backslash should produce single backslash");
// "\\" in double quotes should produce a single backslash
let result = unescape_str(r#""\\""#);
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
assert_eq!(
inner, "\\",
"Double backslash should produce single backslash"
);
}
#[test]
fn dquote_escape_quote() {
// "\"" should produce a literal double quote
let result = unescape_str(r#""\"""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert!(inner.contains('"'), "Escaped quote should produce literal quote");
// "\"" should produce a literal double quote
let result = unescape_str(r#""\"""#);
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
assert!(
inner.contains('"'),
"Escaped quote should produce literal quote"
);
}
#[test]
fn dquote_escape_backtick() {
// "\`" should strip backslash, produce literal backtick
let result = unescape_str(r#""\`""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert_eq!(inner, "`", "Escaped backtick should produce literal backtick");
// "\`" should strip backslash, produce literal backtick
let result = unescape_str(r#""\`""#);
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
assert_eq!(
inner, "`",
"Escaped backtick should produce literal backtick"
);
}
#[test]
fn dquote_escape_nonspecial_preserves_backslash() {
// "\a" inside double quotes should preserve the backslash (a is not special)
let result = unescape_str(r#""\a""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert_eq!(inner, "\\a", "Backslash before non-special char should be preserved");
// "\a" inside double quotes should preserve the backslash (a is not special)
let result = unescape_str(r#""\a""#);
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
assert_eq!(
inner, "\\a",
"Backslash before non-special char should be preserved"
);
}
#[test]
fn dquote_unescaped_dollar_expands() {
// "$foo" inside double quotes should produce VAR_SUB (expansion marker)
let result = unescape_str(r#""$foo""#);
assert!(result.contains(VAR_SUB), "Unescaped $ should become VAR_SUB");
// "$foo" inside double quotes should produce VAR_SUB (expansion marker)
let result = unescape_str(r#""$foo""#);
assert!(
result.contains(VAR_SUB),
"Unescaped $ should become VAR_SUB"
);
}
#[test]
fn dquote_mixed_escapes() {
// "hello \$world \\end" should have literal $, single backslash
let result = unescape_str(r#""hello \$world \\end""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
assert!(result.contains('$'), "Literal $ should be in output");
// Should have exactly one backslash (from \\)
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
// "hello \$world \\end" should have literal $, single backslash
let result = unescape_str(r#""hello \$world \\end""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
assert!(result.contains('$'), "Literal $ should be in output");
// Should have exactly one backslash (from \\)
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
}

View File

@@ -1,27 +1,29 @@
use crate::prompt::readline::{
annotate_input, annotate_input_recursive, markers,
highlight::Highlighter,
annotate_input, annotate_input_recursive, highlight::Highlighter, markers,
};
use super::*;
/// Helper to check if a marker exists at any position in the annotated string
fn has_marker(annotated: &str, marker: char) -> bool {
annotated.contains(marker)
annotated.contains(marker)
}
/// Helper to find the position of a marker in the annotated string
fn find_marker(annotated: &str, marker: char) -> Option<usize> {
annotated.find(marker)
annotated.find(marker)
}
/// Helper to check if markers appear in the correct order
fn marker_before(annotated: &str, first: char, second: char) -> bool {
if let (Some(pos1), Some(pos2)) = (find_marker(annotated, first), find_marker(annotated, second)) {
pos1 < pos2
} else {
false
}
if let (Some(pos1), Some(pos2)) = (
find_marker(annotated, first),
find_marker(annotated, second),
) {
pos1 < pos2
} else {
false
}
}
// ============================================================================
@@ -30,69 +32,70 @@ fn marker_before(annotated: &str, first: char, second: char) -> bool {
#[test]
fn annotate_simple_command() {
let input = "/bin/ls -la";
let annotated = annotate_input(input);
let input = "/bin/ls -la";
let annotated = annotate_input(input);
// Should have COMMAND marker for "/bin/ls" (external command)
assert!(has_marker(&annotated, markers::COMMAND));
// Should have COMMAND marker for "/bin/ls" (external command)
assert!(has_marker(&annotated, markers::COMMAND));
// Should have ARG marker for "-la"
assert!(has_marker(&annotated, markers::ARG));
// Should have ARG marker for "-la"
assert!(has_marker(&annotated, markers::ARG));
// Should have RESET markers
assert!(has_marker(&annotated, markers::RESET));
// Should have RESET markers
assert!(has_marker(&annotated, markers::RESET));
}
#[test]
fn annotate_builtin_command() {
let input = "export FOO=bar";
let annotated = annotate_input(input);
let input = "export FOO=bar";
let annotated = annotate_input(input);
// Should mark "export" as BUILTIN
assert!(has_marker(&annotated, markers::BUILTIN));
// Should mark "export" as BUILTIN
assert!(has_marker(&annotated, markers::BUILTIN));
// Should mark assignment (or ARG if assignment isn't specifically marked separately)
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
// Should mark assignment (or ARG if assignment isn't specifically marked
// separately)
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
}
#[test]
fn annotate_operator() {
let input = "ls | grep foo";
let annotated = annotate_input(input);
let input = "ls | grep foo";
let annotated = annotate_input(input);
// Should have OPERATOR marker for pipe
assert!(has_marker(&annotated, markers::OPERATOR));
// Should have OPERATOR marker for pipe
assert!(has_marker(&annotated, markers::OPERATOR));
// Should have COMMAND markers for both commands
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert_eq!(command_count, 2);
// Should have COMMAND markers for both commands
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert_eq!(command_count, 2);
}
#[test]
fn annotate_redirect() {
let input = "echo hello > output.txt";
let annotated = annotate_input(input);
let input = "echo hello > output.txt";
let annotated = annotate_input(input);
// Should have REDIRECT marker
assert!(has_marker(&annotated, markers::REDIRECT));
// Should have REDIRECT marker
assert!(has_marker(&annotated, markers::REDIRECT));
}
#[test]
fn annotate_keyword() {
let input = "if true; then echo yes; fi";
let annotated = annotate_input(input);
let input = "if true; then echo yes; fi";
let annotated = annotate_input(input);
// Should have KEYWORD markers for if/then/fi
assert!(has_marker(&annotated, markers::KEYWORD));
// Should have KEYWORD markers for if/then/fi
assert!(has_marker(&annotated, markers::KEYWORD));
}
#[test]
fn annotate_command_separator() {
let input = "echo foo; echo bar";
let annotated = annotate_input(input);
let input = "echo foo; echo bar";
let annotated = annotate_input(input);
// Should have CMD_SEP marker for semicolon
assert!(has_marker(&annotated, markers::CMD_SEP));
// Should have CMD_SEP marker for semicolon
assert!(has_marker(&annotated, markers::CMD_SEP));
}
// ============================================================================
@@ -101,83 +104,87 @@ fn annotate_command_separator() {
#[test]
fn annotate_variable_simple() {
let input = "echo $foo";
let annotated = annotate_input(input);
let input = "echo $foo";
let annotated = annotate_input(input);
// Should have VAR_SUB markers
assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END));
// Should have VAR_SUB markers
assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END));
}
#[test]
fn annotate_variable_braces() {
let input = "echo ${foo}";
let annotated = annotate_input(input);
let input = "echo ${foo}";
let annotated = annotate_input(input);
// Should have VAR_SUB markers for ${foo}
assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END));
// Should have VAR_SUB markers for ${foo}
assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END));
}
#[test]
fn annotate_double_quoted_string() {
let input = r#"echo "hello world""#;
let annotated = annotate_input(input);
let input = r#"echo "hello world""#;
let annotated = annotate_input(input);
// Should have STRING_DQ markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END));
// Should have STRING_DQ markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END));
}
#[test]
fn annotate_single_quoted_string() {
let input = "echo 'hello world'";
let annotated = annotate_input(input);
let input = "echo 'hello world'";
let annotated = annotate_input(input);
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
assert!(has_marker(&annotated, markers::STRING_SQ_END));
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
assert!(has_marker(&annotated, markers::STRING_SQ_END));
}
#[test]
fn annotate_variable_in_string() {
let input = r#"echo "hello $USER""#;
let annotated = annotate_input(input);
let input = r#"echo "hello $USER""#;
let annotated = annotate_input(input);
// Should have both STRING_DQ and VAR_SUB markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::VAR_SUB));
// Should have both STRING_DQ and VAR_SUB markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::VAR_SUB));
// VAR_SUB should be inside STRING_DQ
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB));
// VAR_SUB should be inside STRING_DQ
assert!(marker_before(
&annotated,
markers::STRING_DQ,
markers::VAR_SUB
));
}
#[test]
fn annotate_glob_asterisk() {
let input = "ls *.txt";
let annotated = annotate_input(input);
let input = "ls *.txt";
let annotated = annotate_input(input);
// Should have GLOB marker for *
assert!(has_marker(&annotated, markers::GLOB));
// Should have GLOB marker for *
assert!(has_marker(&annotated, markers::GLOB));
}
#[test]
fn annotate_glob_question() {
let input = "ls file?.txt";
let annotated = annotate_input(input);
let input = "ls file?.txt";
let annotated = annotate_input(input);
// Should have GLOB marker for ?
assert!(has_marker(&annotated, markers::GLOB));
// Should have GLOB marker for ?
assert!(has_marker(&annotated, markers::GLOB));
}
#[test]
fn annotate_glob_bracket() {
let input = "ls file[abc].txt";
let annotated = annotate_input(input);
let input = "ls file[abc].txt";
let annotated = annotate_input(input);
// Should have GLOB markers for bracket expression
let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count();
assert!(glob_count >= 2); // Opening and closing
// Should have GLOB markers for bracket expression
let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count();
assert!(glob_count >= 2); // Opening and closing
}
// ============================================================================
@@ -186,32 +193,32 @@ fn annotate_glob_bracket() {
#[test]
fn annotate_command_sub_basic() {
let input = "echo $(whoami)";
let annotated = annotate_input(input);
let input = "echo $(whoami)";
let annotated = annotate_input(input);
// Should have CMD_SUB markers (but not recursively annotated yet)
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
// Should have CMD_SUB markers (but not recursively annotated yet)
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
}
#[test]
fn annotate_subshell_basic() {
let input = "(cd /tmp && ls)";
let annotated = annotate_input(input);
let input = "(cd /tmp && ls)";
let annotated = annotate_input(input);
// Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END));
// Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END));
}
#[test]
fn annotate_process_sub_output() {
let input = "diff <(ls dir1) <(ls dir2)";
let annotated = annotate_input(input);
let input = "diff <(ls dir1) <(ls dir2)";
let annotated = annotate_input(input);
// Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB));
assert!(has_marker(&annotated, markers::PROC_SUB_END));
// Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB));
assert!(has_marker(&annotated, markers::PROC_SUB_END));
}
// ============================================================================
@@ -220,88 +227,97 @@ fn annotate_process_sub_output() {
#[test]
fn annotate_recursive_command_sub() {
let input = "echo $(whoami)";
let annotated = annotate_input_recursive(input);
let input = "echo $(whoami)";
let annotated = annotate_input_recursive(input);
// Should have CMD_SUB markers
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
// Should have CMD_SUB markers
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
// Inside the command sub, "whoami" should be marked as COMMAND
// The recursive annotator should have processed the inside
assert!(has_marker(&annotated, markers::COMMAND));
// Inside the command sub, "whoami" should be marked as COMMAND
// The recursive annotator should have processed the inside
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn annotate_recursive_nested_command_sub() {
let input = "echo $(echo $(whoami))";
let annotated = annotate_input_recursive(input);
let input = "echo $(echo $(whoami))";
let annotated = annotate_input_recursive(input);
// Should have multiple CMD_SUB markers (nested)
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
assert!(cmd_sub_count >= 2, "Should have at least 2 CMD_SUB markers for nested substitutions");
// Should have multiple CMD_SUB markers (nested)
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
assert!(
cmd_sub_count >= 2,
"Should have at least 2 CMD_SUB markers for nested substitutions"
);
}
#[test]
fn annotate_recursive_command_sub_with_args() {
let input = "echo $(grep foo file.txt)";
let annotated = annotate_input_recursive(input);
let input = "echo $(grep foo file.txt)";
let annotated = annotate_input_recursive(input);
// Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH)
// Just check that we have command-type markers
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count();
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert!(builtin_count + command_count >= 2, "Expected at least 2 command markers (BUILTIN or COMMAND)");
// Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH)
// Just check that we have command-type markers
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count();
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert!(
builtin_count + command_count >= 2,
"Expected at least 2 command markers (BUILTIN or COMMAND)"
);
}
#[test]
fn annotate_recursive_subshell() {
let input = "(echo hello; echo world)";
let annotated = annotate_input_recursive(input);
let input = "(echo hello; echo world)";
let annotated = annotate_input_recursive(input);
// Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END));
// Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END));
// Inside should be annotated with BUILTIN (echo is a builtin) and CMD_SEP
assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::CMD_SEP));
// Inside should be annotated with BUILTIN (echo is a builtin) and CMD_SEP
assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::CMD_SEP));
}
#[test]
fn annotate_recursive_process_sub() {
let input = "diff <(ls -la)";
let annotated = annotate_input_recursive(input);
let input = "diff <(ls -la)";
let annotated = annotate_input_recursive(input);
// Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB));
// Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB));
// ls should be marked as COMMAND inside the process sub
assert!(has_marker(&annotated, markers::COMMAND));
// ls should be marked as COMMAND inside the process sub
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn annotate_recursive_command_sub_in_string() {
let input = r#"echo "current user: $(whoami)""#;
let annotated = annotate_input_recursive(input);
let input = r#"echo "current user: $(whoami)""#;
let annotated = annotate_input_recursive(input);
// Should have STRING_DQ, CMD_SUB, and COMMAND markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::COMMAND));
// Should have STRING_DQ, CMD_SUB, and COMMAND markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn annotate_recursive_deeply_nested() {
let input = r#"echo "outer: $(echo "inner: $(whoami)")""#;
let annotated = annotate_input_recursive(input);
let input = r#"echo "outer: $(echo "inner: $(whoami)")""#;
let annotated = annotate_input_recursive(input);
// Should have multiple STRING_DQ and CMD_SUB markers
let string_count = annotated.chars().filter(|&c| c == markers::STRING_DQ).count();
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
// Should have multiple STRING_DQ and CMD_SUB markers
let string_count = annotated
.chars()
.filter(|&c| c == markers::STRING_DQ)
.count();
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers");
assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers");
}
// ============================================================================
@@ -310,33 +326,37 @@ fn annotate_recursive_deeply_nested() {
#[test]
fn marker_priority_var_in_string() {
let input = r#""$foo""#;
let annotated = annotate_input(input);
let input = r#""$foo""#;
let annotated = annotate_input(input);
// STRING_DQ should come before VAR_SUB (outer before inner)
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB));
// STRING_DQ should come before VAR_SUB (outer before inner)
assert!(marker_before(
&annotated,
markers::STRING_DQ,
markers::VAR_SUB
));
}
#[test]
fn marker_priority_arg_vs_string() {
let input = r#"echo "hello""#;
let annotated = annotate_input(input);
let input = r#"echo "hello""#;
let annotated = annotate_input(input);
// Both ARG and STRING_DQ should be present
// STRING_DQ should be inside the ARG token's span
assert!(has_marker(&annotated, markers::ARG));
assert!(has_marker(&annotated, markers::STRING_DQ));
// Both ARG and STRING_DQ should be present
// STRING_DQ should be inside the ARG token's span
assert!(has_marker(&annotated, markers::ARG));
assert!(has_marker(&annotated, markers::STRING_DQ));
}
#[test]
fn marker_priority_reset_placement() {
let input = "echo hello";
let annotated = annotate_input(input);
let input = "echo hello";
let annotated = annotate_input(input);
// RESET markers should appear after each token
// There should be multiple RESET markers
let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count();
assert!(reset_count >= 2);
// RESET markers should appear after each token
// There should be multiple RESET markers
let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count();
assert!(reset_count >= 2);
}
// ============================================================================
@@ -345,127 +365,131 @@ fn marker_priority_reset_placement() {
#[test]
fn highlighter_produces_ansi_codes() {
let mut highlighter = Highlighter::new();
highlighter.load_input("echo hello");
highlighter.highlight();
let output = highlighter.take();
let mut highlighter = Highlighter::new();
highlighter.load_input("echo hello");
highlighter.highlight();
let output = highlighter.take();
// Should contain ANSI escape codes
assert!(output.contains("\x1b["), "Output should contain ANSI escape sequences");
// Should contain ANSI escape codes
assert!(
output.contains("\x1b["),
"Output should contain ANSI escape sequences"
);
// Should still contain the original text
assert!(output.contains("echo"));
assert!(output.contains("hello"));
// Should still contain the original text
assert!(output.contains("echo"));
assert!(output.contains("hello"));
}
#[test]
fn highlighter_handles_empty_input() {
let mut highlighter = Highlighter::new();
highlighter.load_input("");
highlighter.highlight();
let output = highlighter.take();
let mut highlighter = Highlighter::new();
highlighter.load_input("");
highlighter.highlight();
let output = highlighter.take();
// Should not crash and should return empty or minimal output
assert!(output.len() < 10); // Just escape codes or empty
// Should not crash and should return empty or minimal output
assert!(output.len() < 10); // Just escape codes or empty
}
#[test]
fn highlighter_command_validation() {
let mut highlighter = Highlighter::new();
let mut highlighter = Highlighter::new();
// Valid command (echo exists)
highlighter.load_input("echo test");
highlighter.highlight();
let valid_output = highlighter.take();
// Valid command (echo exists)
highlighter.load_input("echo test");
highlighter.highlight();
let valid_output = highlighter.take();
// Invalid command (definitely doesn't exist)
highlighter.load_input("xyznotacommand123 test");
highlighter.highlight();
let invalid_output = highlighter.take();
// Invalid command (definitely doesn't exist)
highlighter.load_input("xyznotacommand123 test");
highlighter.highlight();
let invalid_output = highlighter.take();
// Both should have ANSI codes
assert!(valid_output.contains("\x1b["));
assert!(invalid_output.contains("\x1b["));
// Both should have ANSI codes
assert!(valid_output.contains("\x1b["));
assert!(invalid_output.contains("\x1b["));
// The color codes should be different (green vs red)
// Valid commands should have \x1b[32m (green)
// Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red)
// The color codes should be different (green vs red)
// Valid commands should have \x1b[32m (green)
// Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red)
}
#[test]
fn highlighter_preserves_text_content() {
let input = "echo hello world";
let mut highlighter = Highlighter::new();
highlighter.load_input(input);
highlighter.highlight();
let output = highlighter.take();
let input = "echo hello world";
let mut highlighter = Highlighter::new();
highlighter.load_input(input);
highlighter.highlight();
let output = highlighter.take();
// Remove ANSI codes to check text content
let text_only: String = output.chars()
.filter(|c| !c.is_control() && *c != '\x1b')
.collect();
// Remove ANSI codes to check text content
let text_only: String = output
.chars()
.filter(|c| !c.is_control() && *c != '\x1b')
.collect();
// Should still contain the words (might have escape sequence fragments)
assert!(output.contains("echo"));
assert!(output.contains("hello"));
assert!(output.contains("world"));
// Should still contain the words (might have escape sequence fragments)
assert!(output.contains("echo"));
assert!(output.contains("hello"));
assert!(output.contains("world"));
}
#[test]
fn highlighter_multiple_tokens() {
let mut highlighter = Highlighter::new();
highlighter.load_input("ls -la | grep foo");
highlighter.highlight();
let output = highlighter.take();
let mut highlighter = Highlighter::new();
highlighter.load_input("ls -la | grep foo");
highlighter.highlight();
let output = highlighter.take();
// Should contain all tokens
assert!(output.contains("ls"));
assert!(output.contains("-la"));
assert!(output.contains("|"));
assert!(output.contains("grep"));
assert!(output.contains("foo"));
// Should contain all tokens
assert!(output.contains("ls"));
assert!(output.contains("-la"));
assert!(output.contains("|"));
assert!(output.contains("grep"));
assert!(output.contains("foo"));
// Should have ANSI codes
assert!(output.contains("\x1b["));
// Should have ANSI codes
assert!(output.contains("\x1b["));
}
#[test]
fn highlighter_string_with_variable() {
let mut highlighter = Highlighter::new();
highlighter.load_input(r#"echo "hello $USER""#);
highlighter.highlight();
let output = highlighter.take();
let mut highlighter = Highlighter::new();
highlighter.load_input(r#"echo "hello $USER""#);
highlighter.highlight();
let output = highlighter.take();
// Should contain the text
assert!(output.contains("echo"));
assert!(output.contains("hello"));
assert!(output.contains("USER"));
// Should contain the text
assert!(output.contains("echo"));
assert!(output.contains("hello"));
assert!(output.contains("USER"));
// Should have ANSI codes for different elements
assert!(output.contains("\x1b["));
// Should have ANSI codes for different elements
assert!(output.contains("\x1b["));
}
#[test]
fn highlighter_reusable() {
let mut highlighter = Highlighter::new();
let mut highlighter = Highlighter::new();
// First input
highlighter.load_input("echo first");
highlighter.highlight();
let output1 = highlighter.take();
// First input
highlighter.load_input("echo first");
highlighter.highlight();
let output1 = highlighter.take();
// Second input (reusing same highlighter)
highlighter.load_input("echo second");
highlighter.highlight();
let output2 = highlighter.take();
// Second input (reusing same highlighter)
highlighter.load_input("echo second");
highlighter.highlight();
let output2 = highlighter.take();
// Both should work
assert!(output1.contains("first"));
assert!(output2.contains("second"));
// Both should work
assert!(output1.contains("first"));
assert!(output2.contains("second"));
// Should not contain each other's text
assert!(!output1.contains("second"));
assert!(!output2.contains("first"));
// Should not contain each other's text
assert!(!output1.contains("second"));
assert!(!output2.contains("first"));
}
// ============================================================================
@@ -474,133 +498,143 @@ fn highlighter_reusable() {
#[test]
fn annotate_unclosed_string() {
let input = r#"echo "hello"#;
let annotated = annotate_input(input);
let input = r#"echo "hello"#;
let annotated = annotate_input(input);
// Should handle unclosed string gracefully
assert!(has_marker(&annotated, markers::STRING_DQ));
// May or may not have STRING_DQ_END depending on implementation
// Should handle unclosed string gracefully
assert!(has_marker(&annotated, markers::STRING_DQ));
// May or may not have STRING_DQ_END depending on implementation
}
#[test]
fn annotate_unclosed_command_sub() {
let input = "echo $(whoami";
let annotated = annotate_input(input);
let input = "echo $(whoami";
let annotated = annotate_input(input);
// Should handle unclosed command sub gracefully
assert!(has_marker(&annotated, markers::CMD_SUB));
// Should handle unclosed command sub gracefully
assert!(has_marker(&annotated, markers::CMD_SUB));
}
#[test]
fn annotate_empty_command_sub() {
let input = "echo $()";
let annotated = annotate_input_recursive(input);
let input = "echo $()";
let annotated = annotate_input_recursive(input);
// Should handle empty command sub
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
// Should handle empty command sub
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
}
#[test]
fn annotate_escaped_characters() {
let input = r#"echo \$foo \`bar\` \"test\""#;
let annotated = annotate_input(input);
let input = r#"echo \$foo \`bar\` \"test\""#;
let annotated = annotate_input(input);
// Should not mark escaped $ as variable
// This is tricky - the behavior depends on implementation
// At minimum, should not crash
// Should not mark escaped $ as variable
// This is tricky - the behavior depends on implementation
// At minimum, should not crash
}
#[test]
fn annotate_special_variables() {
let input = "echo $0 $1 $2 $3 $4";
let annotated = annotate_input(input);
let input = "echo $0 $1 $2 $3 $4";
let annotated = annotate_input(input);
// Should mark positional parameters
let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count();
assert!(var_count >= 5, "Expected at least 5 VAR_SUB markers, found {}", var_count);
// Should mark positional parameters
let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count();
assert!(
var_count >= 5,
"Expected at least 5 VAR_SUB markers, found {}",
var_count
);
}
#[test]
fn annotate_variable_no_expansion_in_single_quotes() {
let input = "echo '$foo'";
let annotated = annotate_input(input);
let input = "echo '$foo'";
let annotated = annotate_input(input);
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
// Should NOT have VAR_SUB markers (variables don't expand in single quotes)
// Note: The annotator might still mark it - depends on implementation
// Should NOT have VAR_SUB markers (variables don't expand in single quotes)
// Note: The annotator might still mark it - depends on implementation
}
#[test]
fn annotate_complex_pipeline() {
let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq";
let annotated = annotate_input(input);
let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq";
let annotated = annotate_input(input);
// Should have multiple OPERATOR markers for pipes
let operator_count = annotated.chars().filter(|&c| c == markers::OPERATOR).count();
assert!(operator_count >= 4);
// Should have multiple OPERATOR markers for pipes
let operator_count = annotated
.chars()
.filter(|&c| c == markers::OPERATOR)
.count();
assert!(operator_count >= 4);
// Should have multiple COMMAND markers
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert!(command_count >= 5);
// Should have multiple COMMAND markers
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert!(command_count >= 5);
}
#[test]
fn annotate_assignment_with_command_sub() {
let input = "FOO=$(whoami)";
let annotated = annotate_input_recursive(input);
let input = "FOO=$(whoami)";
let annotated = annotate_input_recursive(input);
// Should have ASSIGNMENT marker
assert!(has_marker(&annotated, markers::ASSIGNMENT));
// Should have ASSIGNMENT marker
assert!(has_marker(&annotated, markers::ASSIGNMENT));
// Should have CMD_SUB marker
assert!(has_marker(&annotated, markers::CMD_SUB));
// Should have CMD_SUB marker
assert!(has_marker(&annotated, markers::CMD_SUB));
// Inside command sub should have COMMAND marker
assert!(has_marker(&annotated, markers::COMMAND));
// Inside command sub should have COMMAND marker
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn annotate_redirect_with_fd() {
let input = "command 2>&1";
let annotated = annotate_input(input);
let input = "command 2>&1";
let annotated = annotate_input(input);
// Should have REDIRECT marker for the redirect operator
assert!(has_marker(&annotated, markers::REDIRECT));
// Should have REDIRECT marker for the redirect operator
assert!(has_marker(&annotated, markers::REDIRECT));
}
#[test]
fn annotate_multiple_redirects() {
let input = "command > out.txt 2>&1";
let annotated = annotate_input(input);
let input = "command > out.txt 2>&1";
let annotated = annotate_input(input);
// Should have multiple REDIRECT markers
let redirect_count = annotated.chars().filter(|&c| c == markers::REDIRECT).count();
assert!(redirect_count >= 2);
// Should have multiple REDIRECT markers
let redirect_count = annotated
.chars()
.filter(|&c| c == markers::REDIRECT)
.count();
assert!(redirect_count >= 2);
}
#[test]
fn annotate_here_string() {
let input = "cat <<< 'hello world'";
let annotated = annotate_input(input);
let input = "cat <<< 'hello world'";
let annotated = annotate_input(input);
// Should have REDIRECT marker for <<<
assert!(has_marker(&annotated, markers::REDIRECT));
// Should have REDIRECT marker for <<<
assert!(has_marker(&annotated, markers::REDIRECT));
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
}
#[test]
fn annotate_unicode_content() {
let input = "echo 'hello 世界 🌍'";
let annotated = annotate_input(input);
let input = "echo 'hello 世界 🌍'";
let annotated = annotate_input(input);
// Should handle unicode gracefully
assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::STRING_SQ));
// Should handle unicode gracefully
assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::STRING_SQ));
}
// ============================================================================
@@ -609,26 +643,26 @@ fn annotate_unicode_content() {
#[test]
fn regression_arg_marker_at_position_zero() {
// Regression test: ARG marker was appearing at position 3 for input "ech"
// This was caused by SOI/EOI tokens falling through to ARG annotation
let input = "ech";
let annotated = annotate_input(input);
// Regression test: ARG marker was appearing at position 3 for input "ech"
// This was caused by SOI/EOI tokens falling through to ARG annotation
let input = "ech";
let annotated = annotate_input(input);
// Should only have COMMAND marker, not ARG
// (incomplete command should still be marked as command attempt)
assert!(has_marker(&annotated, markers::COMMAND));
// Should only have COMMAND marker, not ARG
// (incomplete command should still be marked as command attempt)
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn regression_string_color_in_annotated_strings() {
// Regression test: ARG marker was overriding STRING_DQ color
let input = r#"echo "test""#;
let annotated = annotate_input(input);
// Regression test: ARG marker was overriding STRING_DQ color
let input = r#"echo "test""#;
let annotated = annotate_input(input);
// STRING_DQ should be present and properly positioned
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END));
// STRING_DQ should be present and properly positioned
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END));
// The string markers should come after the ARG marker
// (so they override it in the highlighting)
// The string markers should come after the ARG marker
// (so they override it in the highlighting)
}

View File

@@ -4,8 +4,9 @@ use super::*;
use crate::expand::{expand_aliases, unescape_str};
use crate::libsh::error::{Note, ShErr, ShErrKind};
use crate::parse::{
NdRule, Node, ParseStream,
lex::{LexFlags, LexStream, Tk, TkRule},
node_operation, NdRule, Node, ParseStream,
node_operation,
};
use crate::state::{write_logic, write_vars};

View File

@@ -1,14 +1,17 @@
use std::collections::VecDeque;
use crate::{
libsh::{error::ShErr, term::{Style, Styled}},
libsh::{
error::ShErr,
term::{Style, Styled},
},
prompt::readline::{
FernVi,
history::History,
keys::{KeyCode, KeyEvent, ModKeys},
linebuf::LineBuf,
term::{raw_mode, KeyReader, LineWriter},
term::{KeyReader, LineWriter, raw_mode},
vimode::{ViInsert, ViMode, ViNormal},
FernVi,
},
};
@@ -173,8 +176,9 @@ impl LineWriter for TestWriter {
}
}
// NOTE: FernVi structure has changed significantly and readline() method no longer exists
// These test helpers are disabled until they can be properly updated
// NOTE: FernVi structure has changed significantly and readline() method no
// longer exists These test helpers are disabled until they can be properly
// updated
/*
impl FernVi {
pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self {
@@ -612,10 +616,10 @@ fn fernvi_test_mode_change() {
#[test]
fn fernvi_test_lorem_ipsum_1() {
assert_eq!(fernvi_test(
"\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r",
LOREM_IPSUM),
"Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
)
"\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r",
LOREM_IPSUM),
"Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
)
}
#[test]
@@ -632,9 +636,9 @@ fn fernvi_test_lorem_ipsum_undo() {
#[test]
fn fernvi_test_lorem_ipsum_ctrl_w() {
assert_eq!(fernvi_test(
"\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r",
LOREM_IPSUM),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
)
"\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r",
LOREM_IPSUM),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
)
}
*/

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use crate::parse::{
lex::{LexFlags, LexStream},
Node, NdRule, ParseStream, RedirType, Redir,
NdRule, Node, ParseStream, Redir, RedirType,
lex::{LexFlags, LexStream},
};
use crate::procio::{IoFrame, IoMode, IoStack};
@@ -11,187 +11,238 @@ use crate::procio::{IoFrame, IoMode, IoStack};
// ============================================================================
fn parse_command(input: &str) -> Node {
let source = Arc::new(input.to_string());
let tokens = LexStream::new(source, LexFlags::empty())
.flatten()
.collect::<Vec<_>>();
let source = Arc::new(input.to_string());
let tokens = LexStream::new(source, LexFlags::empty())
.flatten()
.collect::<Vec<_>>();
let mut nodes = ParseStream::new(tokens)
.flatten()
.collect::<Vec<_>>();
let mut nodes = ParseStream::new(tokens).flatten().collect::<Vec<_>>();
assert_eq!(nodes.len(), 1, "Expected exactly one node");
let top_node = nodes.remove(0);
assert_eq!(nodes.len(), 1, "Expected exactly one node");
let top_node = nodes.remove(0);
// Navigate to the actual Command node within the AST structure
// Structure is typically: Conjunction -> Pipeline -> Command
match top_node.class {
NdRule::Conjunction { elements } => {
let first_element = elements.into_iter().next().expect("Expected at least one conjunction element");
match first_element.cmd.class {
NdRule::Pipeline { cmds, .. } => {
let mut commands = cmds;
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline");
commands.remove(0)
}
NdRule::Command { .. } => *first_element.cmd,
_ => panic!("Expected Command or Pipeline node, got {:?}", first_element.cmd.class),
}
}
NdRule::Pipeline { cmds, .. } => {
let mut commands = cmds;
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline");
commands.remove(0)
}
NdRule::Command { .. } => top_node,
_ => panic!("Expected Conjunction, Pipeline, or Command node, got {:?}", top_node.class),
}
// Navigate to the actual Command node within the AST structure
// Structure is typically: Conjunction -> Pipeline -> Command
match top_node.class {
NdRule::Conjunction { elements } => {
let first_element = elements
.into_iter()
.next()
.expect("Expected at least one conjunction element");
match first_element.cmd.class {
NdRule::Pipeline { cmds, .. } => {
let mut commands = cmds;
assert_eq!(
commands.len(),
1,
"Expected exactly one command in pipeline"
);
commands.remove(0)
}
NdRule::Command { .. } => *first_element.cmd,
_ => panic!(
"Expected Command or Pipeline node, got {:?}",
first_element.cmd.class
),
}
}
NdRule::Pipeline { cmds, .. } => {
let mut commands = cmds;
assert_eq!(
commands.len(),
1,
"Expected exactly one command in pipeline"
);
commands.remove(0)
}
NdRule::Command { .. } => top_node,
_ => panic!(
"Expected Conjunction, Pipeline, or Command node, got {:?}",
top_node.class
),
}
}
#[test]
fn parse_output_redirect() {
let node = parse_command("echo hello > output.txt");
let node = parse_command("echo hello > output.txt");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Output));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
assert!(matches!(redir.class, RedirType::Output));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
}
#[test]
fn parse_append_redirect() {
let node = parse_command("echo hello >> output.txt");
let node = parse_command("echo hello >> output.txt");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Append));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
assert!(matches!(redir.class, RedirType::Append));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
}
#[test]
fn parse_input_redirect() {
let node = parse_command("cat < input.txt");
let node = parse_command("cat < input.txt");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Input));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 0, .. }));
assert!(matches!(redir.class, RedirType::Input));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 0, .. }));
}
#[test]
fn parse_stderr_redirect() {
let node = parse_command("ls 2> errors.txt");
let node = parse_command("ls 2> errors.txt");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Output));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 2, .. }));
assert!(matches!(redir.class, RedirType::Output));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 2, .. }));
}
#[test]
fn parse_stderr_to_stdout() {
let node = parse_command("ls 2>&1");
let node = parse_command("ls 2>&1");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 }));
assert!(matches!(
redir.io_mode,
IoMode::Fd {
tgt_fd: 2,
src_fd: 1
}
));
}
#[test]
fn parse_stdout_to_stderr() {
let node = parse_command("echo test 1>&2");
let node = parse_command("echo test 1>&2");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 1, src_fd: 2 }));
assert!(matches!(
redir.io_mode,
IoMode::Fd {
tgt_fd: 1,
src_fd: 2
}
));
}
#[test]
fn parse_multiple_redirects() {
let node = parse_command("cmd < input.txt > output.txt 2> errors.txt");
let node = parse_command("cmd < input.txt > output.txt 2> errors.txt");
assert_eq!(node.redirs.len(), 3);
assert_eq!(node.redirs.len(), 3);
// Input redirect
assert!(matches!(node.redirs[0].class, RedirType::Input));
assert!(matches!(node.redirs[0].io_mode, IoMode::File { tgt_fd: 0, .. }));
// Input redirect
assert!(matches!(node.redirs[0].class, RedirType::Input));
assert!(matches!(
node.redirs[0].io_mode,
IoMode::File { tgt_fd: 0, .. }
));
// Stdout redirect
assert!(matches!(node.redirs[1].class, RedirType::Output));
assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. }));
// Stdout redirect
assert!(matches!(node.redirs[1].class, RedirType::Output));
assert!(matches!(
node.redirs[1].io_mode,
IoMode::File { tgt_fd: 1, .. }
));
// Stderr redirect
assert!(matches!(node.redirs[2].class, RedirType::Output));
assert!(matches!(node.redirs[2].io_mode, IoMode::File { tgt_fd: 2, .. }));
// Stderr redirect
assert!(matches!(node.redirs[2].class, RedirType::Output));
assert!(matches!(
node.redirs[2].io_mode,
IoMode::File { tgt_fd: 2, .. }
));
}
#[test]
fn parse_custom_fd_redirect() {
let node = parse_command("echo test 3> fd3.txt");
let node = parse_command("echo test 3> fd3.txt");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Output));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 3, .. }));
assert!(matches!(redir.class, RedirType::Output));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 3, .. }));
}
#[test]
fn parse_custom_fd_dup() {
let node = parse_command("cmd 3>&4");
let node = parse_command("cmd 3>&4");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 3, src_fd: 4 }));
assert!(matches!(
redir.io_mode,
IoMode::Fd {
tgt_fd: 3,
src_fd: 4
}
));
}
#[test]
fn parse_heredoc() {
let node = parse_command("cat << EOF");
let node = parse_command("cat << EOF");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::HereDoc));
assert!(matches!(redir.class, RedirType::HereDoc));
}
#[test]
fn parse_herestring() {
let node = parse_command("cat <<< 'hello world'");
let node = parse_command("cat <<< 'hello world'");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::HereString));
assert!(matches!(redir.class, RedirType::HereString));
}
#[test]
fn parse_redirect_with_no_space() {
let node = parse_command("echo hello >output.txt");
let node = parse_command("echo hello >output.txt");
assert_eq!(node.redirs.len(), 1);
assert!(matches!(node.redirs[0].class, RedirType::Output));
assert_eq!(node.redirs.len(), 1);
assert!(matches!(node.redirs[0].class, RedirType::Output));
}
#[test]
fn parse_redirect_order_preserved() {
let node = parse_command("cmd 2>&1 > file.txt");
let node = parse_command("cmd 2>&1 > file.txt");
assert_eq!(node.redirs.len(), 2);
assert_eq!(node.redirs.len(), 2);
// First redirect: 2>&1
assert!(matches!(node.redirs[0].io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 }));
// First redirect: 2>&1
assert!(matches!(
node.redirs[0].io_mode,
IoMode::Fd {
tgt_fd: 2,
src_fd: 1
}
));
// Second redirect: > file.txt
assert!(matches!(node.redirs[1].class, RedirType::Output));
assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. }));
// Second redirect: > file.txt
assert!(matches!(node.redirs[1].class, RedirType::Output));
assert!(matches!(
node.redirs[1].io_mode,
IoMode::File { tgt_fd: 1, .. }
));
}
// ============================================================================
@@ -200,148 +251,148 @@ fn parse_redirect_order_preserved() {
#[test]
fn iostack_new() {
let stack = IoStack::new();
let stack = IoStack::new();
assert_eq!(stack.len(), 1, "IoStack should start with one frame");
assert_eq!(stack.curr_frame().len(), 0, "Initial frame should be empty");
assert_eq!(stack.len(), 1, "IoStack should start with one frame");
assert_eq!(stack.curr_frame().len(), 0, "Initial frame should be empty");
}
#[test]
fn iostack_push_pop_frame() {
let mut stack = IoStack::new();
let mut stack = IoStack::new();
// Push a new frame
stack.push_frame(IoFrame::new());
assert_eq!(stack.len(), 2);
// Push a new frame
stack.push_frame(IoFrame::new());
assert_eq!(stack.len(), 2);
// Pop it back
let frame = stack.pop_frame();
assert_eq!(frame.len(), 0);
assert_eq!(stack.len(), 1);
// Pop it back
let frame = stack.pop_frame();
assert_eq!(frame.len(), 0);
assert_eq!(stack.len(), 1);
}
#[test]
fn iostack_never_empties() {
let mut stack = IoStack::new();
let mut stack = IoStack::new();
// Try to pop the last frame
let frame = stack.pop_frame();
assert_eq!(frame.len(), 0);
// Try to pop the last frame
let frame = stack.pop_frame();
assert_eq!(frame.len(), 0);
// Stack should still have one frame
assert_eq!(stack.len(), 1);
// Stack should still have one frame
assert_eq!(stack.len(), 1);
// Pop again - should still have one frame
let frame = stack.pop_frame();
assert_eq!(frame.len(), 0);
assert_eq!(stack.len(), 1);
// Pop again - should still have one frame
let frame = stack.pop_frame();
assert_eq!(frame.len(), 0);
assert_eq!(stack.len(), 1);
}
#[test]
fn iostack_push_to_frame() {
let mut stack = IoStack::new();
let mut stack = IoStack::new();
let redir = crate::parse::Redir::new(
IoMode::fd(1, 2),
RedirType::Output,
);
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
stack.push_to_frame(redir);
assert_eq!(stack.curr_frame().len(), 1);
stack.push_to_frame(redir);
assert_eq!(stack.curr_frame().len(), 1);
}
#[test]
fn iostack_append_to_frame() {
let mut stack = IoStack::new();
let mut stack = IoStack::new();
let redirs = vec![
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
];
let redirs = vec![
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
];
stack.append_to_frame(redirs);
assert_eq!(stack.curr_frame().len(), 2);
stack.append_to_frame(redirs);
assert_eq!(stack.curr_frame().len(), 2);
}
#[test]
fn iostack_frame_isolation() {
let mut stack = IoStack::new();
let mut stack = IoStack::new();
// Add redir to first frame
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
stack.push_to_frame(redir1);
assert_eq!(stack.curr_frame().len(), 1);
// Add redir to first frame
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
stack.push_to_frame(redir1);
assert_eq!(stack.curr_frame().len(), 1);
// Push new frame
stack.push_frame(IoFrame::new());
assert_eq!(stack.curr_frame().len(), 0, "New frame should be empty");
// Push new frame
stack.push_frame(IoFrame::new());
assert_eq!(stack.curr_frame().len(), 0, "New frame should be empty");
// Add redir to second frame
let redir2 = crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output);
stack.push_to_frame(redir2);
assert_eq!(stack.curr_frame().len(), 1);
// Add redir to second frame
let redir2 = crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output);
stack.push_to_frame(redir2);
assert_eq!(stack.curr_frame().len(), 1);
// Pop second frame
let frame2 = stack.pop_frame();
assert_eq!(frame2.len(), 1);
// Pop second frame
let frame2 = stack.pop_frame();
assert_eq!(frame2.len(), 1);
// First frame should still have its redir
assert_eq!(stack.curr_frame().len(), 1);
// First frame should still have its redir
assert_eq!(stack.curr_frame().len(), 1);
}
#[test]
fn iostack_flatten() {
let mut stack = IoStack::new();
let mut stack = IoStack::new();
// Add redir to first frame
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
stack.push_to_frame(redir1);
// Add redir to first frame
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
stack.push_to_frame(redir1);
// Push new frame with redir
let mut frame2 = IoFrame::new();
frame2.push(crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output));
stack.push_frame(frame2);
// Push new frame with redir
let mut frame2 = IoFrame::new();
frame2.push(crate::parse::Redir::new(
IoMode::fd(2, 1),
RedirType::Output,
));
stack.push_frame(frame2);
// Push third frame with redir
let mut frame3 = IoFrame::new();
frame3.push(crate::parse::Redir::new(IoMode::fd(0, 3), RedirType::Input));
stack.push_frame(frame3);
// Push third frame with redir
let mut frame3 = IoFrame::new();
frame3.push(crate::parse::Redir::new(IoMode::fd(0, 3), RedirType::Input));
stack.push_frame(frame3);
assert_eq!(stack.len(), 3);
assert_eq!(stack.len(), 3);
// Flatten
stack.flatten();
// Flatten
stack.flatten();
// Should have one frame with all redirects
assert_eq!(stack.len(), 1);
assert_eq!(stack.curr_frame().len(), 3);
// Should have one frame with all redirects
assert_eq!(stack.len(), 1);
assert_eq!(stack.curr_frame().len(), 3);
}
#[test]
fn ioframe_new() {
let frame = IoFrame::new();
assert_eq!(frame.len(), 0);
let frame = IoFrame::new();
assert_eq!(frame.len(), 0);
}
#[test]
fn ioframe_from_redirs() {
let redirs = vec![
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
];
let redirs = vec![
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
];
let frame = IoFrame::from_redirs(redirs);
assert_eq!(frame.len(), 2);
let frame = IoFrame::from_redirs(redirs);
assert_eq!(frame.len(), 2);
}
#[test]
fn ioframe_push() {
let mut frame = IoFrame::new();
let mut frame = IoFrame::new();
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
frame.push(redir);
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
frame.push(redir);
assert_eq!(frame.len(), 1);
assert_eq!(frame.len(), 1);
}
// ============================================================================
@@ -350,28 +401,28 @@ fn ioframe_push() {
#[test]
fn iomode_fd_construction() {
let io_mode = IoMode::fd(2, 1);
let io_mode = IoMode::fd(2, 1);
match io_mode {
IoMode::Fd { tgt_fd, src_fd } => {
assert_eq!(tgt_fd, 2);
assert_eq!(src_fd, 1);
}
_ => panic!("Expected IoMode::Fd"),
}
match io_mode {
IoMode::Fd { tgt_fd, src_fd } => {
assert_eq!(tgt_fd, 2);
assert_eq!(src_fd, 1);
}
_ => panic!("Expected IoMode::Fd"),
}
}
#[test]
fn iomode_tgt_fd() {
let fd_mode = IoMode::fd(2, 1);
assert_eq!(fd_mode.tgt_fd(), 2);
let fd_mode = IoMode::fd(2, 1);
assert_eq!(fd_mode.tgt_fd(), 2);
let file_mode = IoMode::file(1, std::path::PathBuf::from("test.txt"), RedirType::Output);
assert_eq!(file_mode.tgt_fd(), 1);
let file_mode = IoMode::file(1, std::path::PathBuf::from("test.txt"), RedirType::Output);
assert_eq!(file_mode.tgt_fd(), 1);
}
#[test]
fn iomode_src_fd() {
let fd_mode = IoMode::fd(2, 1);
assert_eq!(fd_mode.src_fd(), 1);
let fd_mode = IoMode::fd(2, 1);
assert_eq!(fd_mode.src_fd(), 1);
}

View File

@@ -6,264 +6,280 @@ use crate::state::{LogTab, ScopeStack, ShellParam, VarFlags, VarTab};
#[test]
fn scopestack_new() {
let stack = ScopeStack::new();
let stack = ScopeStack::new();
// Should start with one global scope
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check it doesn't panic
// Should start with one global scope
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check
// it doesn't
// panic
}
#[test]
fn scopestack_descend_ascend() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
// Set a global variable
stack.set_var("GLOBAL", "value1", VarFlags::NONE);
assert_eq!(stack.get_var("GLOBAL"), "value1");
// Set a global variable
stack.set_var("GLOBAL", "value1", VarFlags::NONE);
assert_eq!(stack.get_var("GLOBAL"), "value1");
// Descend into a new scope
stack.descend(None);
// Descend into a new scope
stack.descend(None);
// Global should still be visible
assert_eq!(stack.get_var("GLOBAL"), "value1");
// Global should still be visible
assert_eq!(stack.get_var("GLOBAL"), "value1");
// Set a local variable
stack.set_var("LOCAL", "value2", VarFlags::LOCAL);
assert_eq!(stack.get_var("LOCAL"), "value2");
// Set a local variable
stack.set_var("LOCAL", "value2", VarFlags::LOCAL);
assert_eq!(stack.get_var("LOCAL"), "value2");
// Ascend back to global scope
stack.ascend();
// Ascend back to global scope
stack.ascend();
// Global should still exist
assert_eq!(stack.get_var("GLOBAL"), "value1");
// Global should still exist
assert_eq!(stack.get_var("GLOBAL"), "value1");
// Local should no longer be visible
assert_eq!(stack.get_var("LOCAL"), "");
// Local should no longer be visible
assert_eq!(stack.get_var("LOCAL"), "");
}
#[test]
fn scopestack_variable_shadowing() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
// Set global variable
stack.set_var("VAR", "global", VarFlags::NONE);
assert_eq!(stack.get_var("VAR"), "global");
// Set global variable
stack.set_var("VAR", "global", VarFlags::NONE);
assert_eq!(stack.get_var("VAR"), "global");
// Descend into local scope
stack.descend(None);
// Descend into local scope
stack.descend(None);
// Set local variable with same name
stack.set_var("VAR", "local", VarFlags::LOCAL);
assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global");
// Set local variable with same name
stack.set_var("VAR", "local", VarFlags::LOCAL);
assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global");
// Ascend back
stack.ascend();
// Ascend back
stack.ascend();
// Global should be restored
assert_eq!(stack.get_var("VAR"), "global", "Global should be unchanged after ascend");
// Global should be restored
assert_eq!(
stack.get_var("VAR"),
"global",
"Global should be unchanged after ascend"
);
}
#[test]
fn scopestack_local_vs_global_flag() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
// Descend into a local scope
stack.descend(None);
// Descend into a local scope
stack.descend(None);
// Set with LOCAL flag - should go in current scope
stack.set_var("LOCAL_VAR", "local", VarFlags::LOCAL);
// Set with LOCAL flag - should go in current scope
stack.set_var("LOCAL_VAR", "local", VarFlags::LOCAL);
// Set without LOCAL flag - should go in global scope
stack.set_var("GLOBAL_VAR", "global", VarFlags::NONE);
// Set without LOCAL flag - should go in global scope
stack.set_var("GLOBAL_VAR", "global", VarFlags::NONE);
// Both visible from local scope
assert_eq!(stack.get_var("LOCAL_VAR"), "local");
assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
// Both visible from local scope
assert_eq!(stack.get_var("LOCAL_VAR"), "local");
assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
// Ascend to global
stack.ascend();
// Ascend to global
stack.ascend();
// Only global var should be visible
assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
assert_eq!(stack.get_var("LOCAL_VAR"), "");
// Only global var should be visible
assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
assert_eq!(stack.get_var("LOCAL_VAR"), "");
}
#[test]
fn scopestack_multiple_levels() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
stack.set_var("LEVEL0", "global", VarFlags::NONE);
stack.set_var("LEVEL0", "global", VarFlags::NONE);
// Level 1
stack.descend(None);
stack.set_var("LEVEL1", "first", VarFlags::LOCAL);
// Level 1
stack.descend(None);
stack.set_var("LEVEL1", "first", VarFlags::LOCAL);
// Level 2
stack.descend(None);
stack.set_var("LEVEL2", "second", VarFlags::LOCAL);
// Level 2
stack.descend(None);
stack.set_var("LEVEL2", "second", VarFlags::LOCAL);
// All variables visible from deepest scope
assert_eq!(stack.get_var("LEVEL0"), "global");
assert_eq!(stack.get_var("LEVEL1"), "first");
assert_eq!(stack.get_var("LEVEL2"), "second");
// All variables visible from deepest scope
assert_eq!(stack.get_var("LEVEL0"), "global");
assert_eq!(stack.get_var("LEVEL1"), "first");
assert_eq!(stack.get_var("LEVEL2"), "second");
// Ascend to level 1
stack.ascend();
assert_eq!(stack.get_var("LEVEL0"), "global");
assert_eq!(stack.get_var("LEVEL1"), "first");
assert_eq!(stack.get_var("LEVEL2"), "");
// Ascend to level 1
stack.ascend();
assert_eq!(stack.get_var("LEVEL0"), "global");
assert_eq!(stack.get_var("LEVEL1"), "first");
assert_eq!(stack.get_var("LEVEL2"), "");
// Ascend to global
stack.ascend();
assert_eq!(stack.get_var("LEVEL0"), "global");
assert_eq!(stack.get_var("LEVEL1"), "");
assert_eq!(stack.get_var("LEVEL2"), "");
// Ascend to global
stack.ascend();
assert_eq!(stack.get_var("LEVEL0"), "global");
assert_eq!(stack.get_var("LEVEL1"), "");
assert_eq!(stack.get_var("LEVEL2"), "");
}
#[test]
fn scopestack_cannot_ascend_past_global() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
stack.set_var("VAR", "value", VarFlags::NONE);
stack.set_var("VAR", "value", VarFlags::NONE);
// Try to ascend from global scope (should be no-op)
stack.ascend();
stack.ascend();
stack.ascend();
// Try to ascend from global scope (should be no-op)
stack.ascend();
stack.ascend();
stack.ascend();
// Variable should still exist
assert_eq!(stack.get_var("VAR"), "value");
// Variable should still exist
assert_eq!(stack.get_var("VAR"), "value");
}
#[test]
fn scopestack_descend_with_args() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
// Get initial param values from global scope (test process args)
let global_param_1 = stack.get_param(ShellParam::Pos(1));
// Get initial param values from global scope (test process args)
let global_param_1 = stack.get_param(ShellParam::Pos(1));
// Descend with positional parameters
let args = vec!["local_arg1".to_string(), "local_arg2".to_string()];
stack.descend(Some(args));
// Descend with positional parameters
let args = vec!["local_arg1".to_string(), "local_arg2".to_string()];
stack.descend(Some(args));
// In local scope, positional params come from the VarTab created during descend
// VarTab::new() initializes with process args, then our args are appended
// So we check that SOME positional parameter exists (implementation detail may vary)
let local_param = stack.get_param(ShellParam::Pos(1));
assert!(!local_param.is_empty(), "Should have positional parameters in local scope");
// In local scope, positional params come from the VarTab created during descend
// VarTab::new() initializes with process args, then our args are appended
// So we check that SOME positional parameter exists (implementation detail may
// vary)
let local_param = stack.get_param(ShellParam::Pos(1));
assert!(
!local_param.is_empty(),
"Should have positional parameters in local scope"
);
// Ascend back
stack.ascend();
// Ascend back
stack.ascend();
// Should be back to global scope parameters
assert_eq!(stack.get_param(ShellParam::Pos(1)), global_param_1);
// Should be back to global scope parameters
assert_eq!(stack.get_param(ShellParam::Pos(1)), global_param_1);
}
#[test]
fn scopestack_global_parameters() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
// Set global parameters
stack.set_param(ShellParam::Status, "0");
stack.set_param(ShellParam::LastJob, "1234");
// Set global parameters
stack.set_param(ShellParam::Status, "0");
stack.set_param(ShellParam::LastJob, "1234");
assert_eq!(stack.get_param(ShellParam::Status), "0");
assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
assert_eq!(stack.get_param(ShellParam::Status), "0");
assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
// Descend into local scope
stack.descend(None);
// Descend into local scope
stack.descend(None);
// Global parameters should still be visible
assert_eq!(stack.get_param(ShellParam::Status), "0");
assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
// Global parameters should still be visible
assert_eq!(stack.get_param(ShellParam::Status), "0");
assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
// Modify global parameter from local scope
stack.set_param(ShellParam::Status, "1");
assert_eq!(stack.get_param(ShellParam::Status), "1");
// Modify global parameter from local scope
stack.set_param(ShellParam::Status, "1");
assert_eq!(stack.get_param(ShellParam::Status), "1");
// Ascend
stack.ascend();
// Ascend
stack.ascend();
// Global parameter should retain modified value
assert_eq!(stack.get_param(ShellParam::Status), "1");
// Global parameter should retain modified value
assert_eq!(stack.get_param(ShellParam::Status), "1");
}
#[test]
fn scopestack_unset_var() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
stack.set_var("VAR", "value", VarFlags::NONE);
assert_eq!(stack.get_var("VAR"), "value");
stack.set_var("VAR", "value", VarFlags::NONE);
assert_eq!(stack.get_var("VAR"), "value");
stack.unset_var("VAR");
assert_eq!(stack.get_var("VAR"), "");
assert!(!stack.var_exists("VAR"));
stack.unset_var("VAR");
assert_eq!(stack.get_var("VAR"), "");
assert!(!stack.var_exists("VAR"));
}
#[test]
fn scopestack_unset_finds_innermost() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
// Set global
stack.set_var("VAR", "global", VarFlags::NONE);
// Set global
stack.set_var("VAR", "global", VarFlags::NONE);
// Descend and shadow
stack.descend(None);
stack.set_var("VAR", "local", VarFlags::LOCAL);
assert_eq!(stack.get_var("VAR"), "local");
// Descend and shadow
stack.descend(None);
stack.set_var("VAR", "local", VarFlags::LOCAL);
assert_eq!(stack.get_var("VAR"), "local");
// Unset should remove local, revealing global
stack.unset_var("VAR");
assert_eq!(stack.get_var("VAR"), "global");
// Unset should remove local, revealing global
stack.unset_var("VAR");
assert_eq!(stack.get_var("VAR"), "global");
}
#[test]
fn scopestack_export_var() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
stack.set_var("VAR", "value", VarFlags::NONE);
stack.set_var("VAR", "value", VarFlags::NONE);
// Export the variable
stack.export_var("VAR");
// Export the variable
stack.export_var("VAR");
// Variable should still be accessible (flag is internal detail)
assert_eq!(stack.get_var("VAR"), "value");
// Variable should still be accessible (flag is internal detail)
assert_eq!(stack.get_var("VAR"), "value");
}
#[test]
fn scopestack_var_exists() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
assert!(!stack.var_exists("NONEXISTENT"));
assert!(!stack.var_exists("NONEXISTENT"));
stack.set_var("EXISTS", "yes", VarFlags::NONE);
assert!(stack.var_exists("EXISTS"));
stack.set_var("EXISTS", "yes", VarFlags::NONE);
assert!(stack.var_exists("EXISTS"));
stack.descend(None);
assert!(stack.var_exists("EXISTS"), "Global var should be visible in local scope");
stack.descend(None);
assert!(
stack.var_exists("EXISTS"),
"Global var should be visible in local scope"
);
stack.set_var("LOCAL", "yes", VarFlags::LOCAL);
assert!(stack.var_exists("LOCAL"));
stack.set_var("LOCAL", "yes", VarFlags::LOCAL);
assert!(stack.var_exists("LOCAL"));
stack.ascend();
assert!(!stack.var_exists("LOCAL"), "Local var should not exist after ascend");
stack.ascend();
assert!(
!stack.var_exists("LOCAL"),
"Local var should not exist after ascend"
);
}
#[test]
fn scopestack_flatten_vars() {
let mut stack = ScopeStack::new();
let mut stack = ScopeStack::new();
stack.set_var("GLOBAL1", "g1", VarFlags::NONE);
stack.set_var("GLOBAL2", "g2", VarFlags::NONE);
stack.set_var("GLOBAL1", "g1", VarFlags::NONE);
stack.set_var("GLOBAL2", "g2", VarFlags::NONE);
stack.descend(None);
stack.set_var("LOCAL1", "l1", VarFlags::LOCAL);
stack.descend(None);
stack.set_var("LOCAL1", "l1", VarFlags::LOCAL);
let flattened = stack.flatten_vars();
let flattened = stack.flatten_vars();
// Should contain variables from all scopes
assert!(flattened.contains_key("GLOBAL1"));
assert!(flattened.contains_key("GLOBAL2"));
assert!(flattened.contains_key("LOCAL1"));
// Should contain variables from all scopes
assert!(flattened.contains_key("GLOBAL1"));
assert!(flattened.contains_key("GLOBAL2"));
assert!(flattened.contains_key("LOCAL1"));
}
// ============================================================================
@@ -272,78 +288,81 @@ fn scopestack_flatten_vars() {
#[test]
fn logtab_new() {
let logtab = LogTab::new();
assert_eq!(logtab.funcs().len(), 0);
assert_eq!(logtab.aliases().len(), 0);
let logtab = LogTab::new();
assert_eq!(logtab.funcs().len(), 0);
assert_eq!(logtab.aliases().len(), 0);
}
#[test]
fn logtab_insert_get_alias() {
let mut logtab = LogTab::new();
let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la");
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
assert_eq!(logtab.get_alias("nonexistent"), None);
logtab.insert_alias("ll", "ls -la");
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
assert_eq!(logtab.get_alias("nonexistent"), None);
}
#[test]
fn logtab_overwrite_alias() {
let mut logtab = LogTab::new();
let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la");
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
logtab.insert_alias("ll", "ls -la");
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
logtab.insert_alias("ll", "ls -lah");
assert_eq!(logtab.get_alias("ll"), Some("ls -lah".to_string()));
logtab.insert_alias("ll", "ls -lah");
assert_eq!(logtab.get_alias("ll"), Some("ls -lah".to_string()));
}
#[test]
fn logtab_remove_alias() {
let mut logtab = LogTab::new();
let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la");
assert!(logtab.get_alias("ll").is_some());
logtab.insert_alias("ll", "ls -la");
assert!(logtab.get_alias("ll").is_some());
logtab.remove_alias("ll");
assert!(logtab.get_alias("ll").is_none());
logtab.remove_alias("ll");
assert!(logtab.get_alias("ll").is_none());
}
#[test]
fn logtab_clear_aliases() {
let mut logtab = LogTab::new();
let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la");
logtab.insert_alias("la", "ls -A");
logtab.insert_alias("l", "ls -CF");
logtab.insert_alias("ll", "ls -la");
logtab.insert_alias("la", "ls -A");
logtab.insert_alias("l", "ls -CF");
assert_eq!(logtab.aliases().len(), 3);
assert_eq!(logtab.aliases().len(), 3);
logtab.clear_aliases();
assert_eq!(logtab.aliases().len(), 0);
logtab.clear_aliases();
assert_eq!(logtab.aliases().len(), 0);
}
#[test]
fn logtab_multiple_aliases() {
let mut logtab = LogTab::new();
let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la");
logtab.insert_alias("la", "ls -A");
logtab.insert_alias("grep", "grep --color=auto");
logtab.insert_alias("ll", "ls -la");
logtab.insert_alias("la", "ls -A");
logtab.insert_alias("grep", "grep --color=auto");
assert_eq!(logtab.aliases().len(), 3);
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string()));
assert_eq!(logtab.get_alias("grep"), Some("grep --color=auto".to_string()));
assert_eq!(logtab.aliases().len(), 3);
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string()));
assert_eq!(
logtab.get_alias("grep"),
Some("grep --color=auto".to_string())
);
}
// Note: Function tests are limited because ShFunc requires complex setup (parsed AST)
// We'll test the basic storage/retrieval mechanics
// Note: Function tests are limited because ShFunc requires complex setup
// (parsed AST) We'll test the basic storage/retrieval mechanics
#[test]
fn logtab_funcs_empty_initially() {
let logtab = LogTab::new();
assert_eq!(logtab.funcs().len(), 0);
assert!(logtab.get_func("nonexistent").is_none());
let logtab = LogTab::new();
assert_eq!(logtab.funcs().len(), 0);
assert!(logtab.get_func("nonexistent").is_none());
}
// ============================================================================
@@ -352,109 +371,112 @@ fn logtab_funcs_empty_initially() {
#[test]
fn vartab_new() {
let vartab = VarTab::new();
// VarTab initializes with some default params, just check it doesn't panic
assert!(vartab.get_var("NONEXISTENT").is_empty());
let vartab = VarTab::new();
// VarTab initializes with some default params, just check it doesn't panic
assert!(vartab.get_var("NONEXISTENT").is_empty());
}
#[test]
fn vartab_set_get_var() {
let mut vartab = VarTab::new();
let mut vartab = VarTab::new();
vartab.set_var("TEST", "value", VarFlags::NONE);
assert_eq!(vartab.get_var("TEST"), "value");
vartab.set_var("TEST", "value", VarFlags::NONE);
assert_eq!(vartab.get_var("TEST"), "value");
}
#[test]
fn vartab_overwrite_var() {
let mut vartab = VarTab::new();
let mut vartab = VarTab::new();
vartab.set_var("VAR", "value1", VarFlags::NONE);
assert_eq!(vartab.get_var("VAR"), "value1");
vartab.set_var("VAR", "value1", VarFlags::NONE);
assert_eq!(vartab.get_var("VAR"), "value1");
vartab.set_var("VAR", "value2", VarFlags::NONE);
assert_eq!(vartab.get_var("VAR"), "value2");
vartab.set_var("VAR", "value2", VarFlags::NONE);
assert_eq!(vartab.get_var("VAR"), "value2");
}
#[test]
fn vartab_var_exists() {
let mut vartab = VarTab::new();
let mut vartab = VarTab::new();
assert!(!vartab.var_exists("TEST"));
assert!(!vartab.var_exists("TEST"));
vartab.set_var("TEST", "value", VarFlags::NONE);
assert!(vartab.var_exists("TEST"));
vartab.set_var("TEST", "value", VarFlags::NONE);
assert!(vartab.var_exists("TEST"));
}
#[test]
fn vartab_unset_var() {
let mut vartab = VarTab::new();
let mut vartab = VarTab::new();
vartab.set_var("VAR", "value", VarFlags::NONE);
assert!(vartab.var_exists("VAR"));
vartab.set_var("VAR", "value", VarFlags::NONE);
assert!(vartab.var_exists("VAR"));
vartab.unset_var("VAR");
assert!(!vartab.var_exists("VAR"));
assert_eq!(vartab.get_var("VAR"), "");
vartab.unset_var("VAR");
assert!(!vartab.var_exists("VAR"));
assert_eq!(vartab.get_var("VAR"), "");
}
#[test]
fn vartab_export_var() {
let mut vartab = VarTab::new();
let mut vartab = VarTab::new();
vartab.set_var("VAR", "value", VarFlags::NONE);
vartab.export_var("VAR");
vartab.set_var("VAR", "value", VarFlags::NONE);
vartab.export_var("VAR");
// Variable should still be accessible
assert_eq!(vartab.get_var("VAR"), "value");
// Variable should still be accessible
assert_eq!(vartab.get_var("VAR"), "value");
}
#[test]
fn vartab_positional_params() {
let mut vartab = VarTab::new();
let mut vartab = VarTab::new();
// Get the current argv length
let initial_len = vartab.sh_argv().len();
// Get the current argv length
let initial_len = vartab.sh_argv().len();
// Clear and reinitialize with known args
vartab.clear_args(); // This keeps $0 as current exe
// Clear and reinitialize with known args
vartab.clear_args(); // This keeps $0 as current exe
// After clear_args, should have just $0
// Push additional args
vartab.bpush_arg("test_arg1".to_string());
vartab.bpush_arg("test_arg2".to_string());
// After clear_args, should have just $0
// Push additional args
vartab.bpush_arg("test_arg1".to_string());
vartab.bpush_arg("test_arg2".to_string());
// Now sh_argv should be: [exe_path, test_arg1, test_arg2]
// Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2
let final_len = vartab.sh_argv().len();
assert!(final_len > initial_len || final_len >= 1, "Should have arguments");
// Now sh_argv should be: [exe_path, test_arg1, test_arg2]
// Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2
let final_len = vartab.sh_argv().len();
assert!(
final_len > initial_len || final_len >= 1,
"Should have arguments"
);
// Just verify we can retrieve the last args we pushed
let last_idx = final_len - 1;
assert_eq!(vartab.get_param(ShellParam::Pos(last_idx)), "test_arg2");
// Just verify we can retrieve the last args we pushed
let last_idx = final_len - 1;
assert_eq!(vartab.get_param(ShellParam::Pos(last_idx)), "test_arg2");
}
#[test]
fn vartab_shell_argv_operations() {
let mut vartab = VarTab::new();
let mut vartab = VarTab::new();
// Clear initial args and set fresh ones
vartab.clear_args();
// Clear initial args and set fresh ones
vartab.clear_args();
// Push args (clear_args leaves $0, so these become $1, $2, $3)
vartab.bpush_arg("arg1".to_string());
vartab.bpush_arg("arg2".to_string());
vartab.bpush_arg("arg3".to_string());
// Push args (clear_args leaves $0, so these become $1, $2, $3)
vartab.bpush_arg("arg1".to_string());
vartab.bpush_arg("arg2".to_string());
vartab.bpush_arg("arg3".to_string());
// Get initial arg count
let initial_len = vartab.sh_argv().len();
// Get initial arg count
let initial_len = vartab.sh_argv().len();
// Pop first arg (removes $0)
let popped = vartab.fpop_arg();
assert!(popped.is_some());
// Pop first arg (removes $0)
let popped = vartab.fpop_arg();
assert!(popped.is_some());
// Should have one fewer arg
assert_eq!(vartab.sh_argv().len(), initial_len - 1);
// Should have one fewer arg
assert_eq!(vartab.sh_argv().len(), initial_len - 1);
}
// ============================================================================
@@ -463,39 +485,39 @@ fn vartab_shell_argv_operations() {
#[test]
fn varflags_none() {
let flags = VarFlags::NONE;
assert!(!flags.contains(VarFlags::EXPORT));
assert!(!flags.contains(VarFlags::LOCAL));
assert!(!flags.contains(VarFlags::READONLY));
let flags = VarFlags::NONE;
assert!(!flags.contains(VarFlags::EXPORT));
assert!(!flags.contains(VarFlags::LOCAL));
assert!(!flags.contains(VarFlags::READONLY));
}
#[test]
fn varflags_export() {
let flags = VarFlags::EXPORT;
assert!(flags.contains(VarFlags::EXPORT));
assert!(!flags.contains(VarFlags::LOCAL));
let flags = VarFlags::EXPORT;
assert!(flags.contains(VarFlags::EXPORT));
assert!(!flags.contains(VarFlags::LOCAL));
}
#[test]
fn varflags_local() {
let flags = VarFlags::LOCAL;
assert!(!flags.contains(VarFlags::EXPORT));
assert!(flags.contains(VarFlags::LOCAL));
let flags = VarFlags::LOCAL;
assert!(!flags.contains(VarFlags::EXPORT));
assert!(flags.contains(VarFlags::LOCAL));
}
#[test]
fn varflags_combine() {
let flags = VarFlags::EXPORT | VarFlags::LOCAL;
assert!(flags.contains(VarFlags::EXPORT));
assert!(flags.contains(VarFlags::LOCAL));
assert!(!flags.contains(VarFlags::READONLY));
let flags = VarFlags::EXPORT | VarFlags::LOCAL;
assert!(flags.contains(VarFlags::EXPORT));
assert!(flags.contains(VarFlags::LOCAL));
assert!(!flags.contains(VarFlags::READONLY));
}
#[test]
fn varflags_readonly() {
let flags = VarFlags::READONLY;
assert!(flags.contains(VarFlags::READONLY));
assert!(!flags.contains(VarFlags::EXPORT));
let flags = VarFlags::READONLY;
assert!(flags.contains(VarFlags::READONLY));
assert!(!flags.contains(VarFlags::EXPORT));
}
// ============================================================================
@@ -504,49 +526,70 @@ fn varflags_readonly() {
#[test]
fn shellparam_is_global() {
assert!(ShellParam::Status.is_global());
assert!(ShellParam::ShPid.is_global());
assert!(ShellParam::LastJob.is_global());
assert!(ShellParam::ShellName.is_global());
assert!(ShellParam::Status.is_global());
assert!(ShellParam::ShPid.is_global());
assert!(ShellParam::LastJob.is_global());
assert!(ShellParam::ShellName.is_global());
assert!(!ShellParam::Pos(1).is_global());
assert!(!ShellParam::AllArgs.is_global());
assert!(!ShellParam::AllArgsStr.is_global());
assert!(!ShellParam::ArgCount.is_global());
assert!(!ShellParam::Pos(1).is_global());
assert!(!ShellParam::AllArgs.is_global());
assert!(!ShellParam::AllArgsStr.is_global());
assert!(!ShellParam::ArgCount.is_global());
}
#[test]
fn shellparam_from_str() {
assert!(matches!("?".parse::<ShellParam>().unwrap(), ShellParam::Status));
assert!(matches!("$".parse::<ShellParam>().unwrap(), ShellParam::ShPid));
assert!(matches!("!".parse::<ShellParam>().unwrap(), ShellParam::LastJob));
assert!(matches!("0".parse::<ShellParam>().unwrap(), ShellParam::ShellName));
assert!(matches!("@".parse::<ShellParam>().unwrap(), ShellParam::AllArgs));
assert!(matches!("*".parse::<ShellParam>().unwrap(), ShellParam::AllArgsStr));
assert!(matches!("#".parse::<ShellParam>().unwrap(), ShellParam::ArgCount));
assert!(matches!(
"?".parse::<ShellParam>().unwrap(),
ShellParam::Status
));
assert!(matches!(
"$".parse::<ShellParam>().unwrap(),
ShellParam::ShPid
));
assert!(matches!(
"!".parse::<ShellParam>().unwrap(),
ShellParam::LastJob
));
assert!(matches!(
"0".parse::<ShellParam>().unwrap(),
ShellParam::ShellName
));
assert!(matches!(
"@".parse::<ShellParam>().unwrap(),
ShellParam::AllArgs
));
assert!(matches!(
"*".parse::<ShellParam>().unwrap(),
ShellParam::AllArgsStr
));
assert!(matches!(
"#".parse::<ShellParam>().unwrap(),
ShellParam::ArgCount
));
match "1".parse::<ShellParam>().unwrap() {
ShellParam::Pos(n) => assert_eq!(n, 1),
_ => panic!("Expected Pos(1)"),
}
match "1".parse::<ShellParam>().unwrap() {
ShellParam::Pos(n) => assert_eq!(n, 1),
_ => panic!("Expected Pos(1)"),
}
match "42".parse::<ShellParam>().unwrap() {
ShellParam::Pos(n) => assert_eq!(n, 42),
_ => panic!("Expected Pos(42)"),
}
match "42".parse::<ShellParam>().unwrap() {
ShellParam::Pos(n) => assert_eq!(n, 42),
_ => panic!("Expected Pos(42)"),
}
assert!("invalid".parse::<ShellParam>().is_err());
assert!("invalid".parse::<ShellParam>().is_err());
}
#[test]
fn shellparam_display() {
assert_eq!(ShellParam::Status.to_string(), "?");
assert_eq!(ShellParam::ShPid.to_string(), "$");
assert_eq!(ShellParam::LastJob.to_string(), "!");
assert_eq!(ShellParam::ShellName.to_string(), "0");
assert_eq!(ShellParam::AllArgs.to_string(), "@");
assert_eq!(ShellParam::AllArgsStr.to_string(), "*");
assert_eq!(ShellParam::ArgCount.to_string(), "#");
assert_eq!(ShellParam::Pos(1).to_string(), "1");
assert_eq!(ShellParam::Pos(99).to_string(), "99");
assert_eq!(ShellParam::Status.to_string(), "?");
assert_eq!(ShellParam::ShPid.to_string(), "$");
assert_eq!(ShellParam::LastJob.to_string(), "!");
assert_eq!(ShellParam::ShellName.to_string(), "0");
assert_eq!(ShellParam::AllArgs.to_string(), "@");
assert_eq!(ShellParam::AllArgsStr.to_string(), "*");
assert_eq!(ShellParam::ArgCount.to_string(), "#");
assert_eq!(ShellParam::Pos(1).to_string(), "1");
assert_eq!(ShellParam::Pos(99).to_string(), "99");
}