Remove test module and delete all test files

This commit is contained in:
2026-03-05 13:46:55 -05:00
parent 633bc16960
commit 8c91748a7e
14 changed files with 0 additions and 4505 deletions

View File

@@ -16,8 +16,6 @@ pub mod readline;
pub mod shopt; pub mod shopt;
pub mod signal; pub mod signal;
pub mod state; pub mod state;
#[cfg(test)]
pub mod tests;
use std::os::fd::BorrowedFd; use std::os::fd::BorrowedFd;
use std::process::ExitCode; use std::process::ExitCode;

View File

@@ -1,767 +0,0 @@
use std::env;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
use crate::prompt::readline::complete::Completer;
use crate::prompt::readline::markers;
use crate::state::{VarFlags, write_logic, write_vars};
use super::*;
/// Helper to create a temp directory with test files
fn setup_test_files() -> TempDir {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path();
// Create some test files and directories
fs::write(path.join("file1.txt"), "").unwrap();
fs::write(path.join("file2.txt"), "").unwrap();
fs::write(path.join("script.sh"), "").unwrap();
fs::create_dir(path.join("subdir")).unwrap();
fs::write(path.join("subdir/nested.txt"), "").unwrap();
fs::create_dir(path.join("another_dir")).unwrap();
temp_dir
}
/// Helper to create a test directory in current dir for relative path tests
fn setup_local_test_files() -> TempDir {
let temp_dir = tempfile::tempdir_in(".").unwrap();
let path = temp_dir.path();
fs::write(path.join("local1.txt"), "").unwrap();
fs::write(path.join("local2.txt"), "").unwrap();
fs::create_dir(path.join("localdir")).unwrap();
temp_dir
}
// ============================================================================
// Command Completion Tests
// ============================================================================
#[test]
fn complete_command_from_path() {
let mut completer = Completer::new();
// Try to complete "ec" - should find "echo" (which is in PATH)
let line = "ec".to_string();
let cursor_pos = 2;
let result = completer.complete(line, cursor_pos, 1);
assert!(result.is_ok());
let completed = result.unwrap();
// Should have found something
assert!(completed.is_some());
let completed_line = completed.unwrap();
// Should contain "echo"
assert!(completed_line.starts_with("echo") || completer.candidates.iter().any(|c| c == "echo"));
}
#[test]
fn complete_command_builtin() {
let mut completer = Completer::new();
// Try to complete "ex" - should find "export" builtin
let line = "ex".to_string();
let cursor_pos = 2;
let result = completer.complete(line, cursor_pos, 1).unwrap();
assert!(result.is_some());
// Check candidates include "export"
assert!(completer.candidates.iter().any(|c| c == "export"));
}
// NOTE: Disabled - ShFunc constructor requires parsed AST which is complex to
// set up in tests TODO: Re-enable once we have a helper to create test
// functions
/*
#[test]
fn complete_command_function() {
write_logic(|l| {
// Add a test function - would need to parse "test_func() { echo test; }"
// and create proper ShFunc from it
// let func = ...;
// l.insert_func("test_func", func);
let mut completer = Completer::new();
let line = "test_f".to_string();
let cursor_pos = 6;
let result = completer.complete(line, cursor_pos, 1).unwrap();
assert!(result.is_some());
// Should find test_func
assert!(completer.candidates.iter().any(|c| c == "test_func"));
// Cleanup
l.clear_functions();
});
}
*/
#[test]
fn complete_command_alias() {
// Add alias outside of completion call to avoid RefCell borrow conflict
write_logic(|l| {
l.insert_alias("ll", "ls -la");
});
let mut completer = Completer::new();
let line = "l".to_string();
let cursor_pos = 1;
let result = completer.complete(line, cursor_pos, 1).unwrap();
assert!(result.is_some());
// Should find ll and ls
assert!(completer.candidates.iter().any(|c| c == "ll"));
// Cleanup
write_logic(|l| {
l.clear_aliases();
});
}
#[test]
fn complete_command_no_matches() {
let mut completer = Completer::new();
// Try to complete something that definitely doesn't exist
let line = "xyzabc123notacommand".to_string();
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1).unwrap();
// Should return None when no matches
assert!(result.is_none());
}
// ============================================================================
// Filename Completion Tests
// ============================================================================
#[test]
fn complete_filename_basic() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("cat {}/fil", path.display());
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1).unwrap();
assert!(result.is_some());
// Should have file1.txt and file2.txt as candidates
assert!(completer.candidates.len() >= 2);
assert!(completer.candidates.iter().any(|c| c.contains("file1.txt")));
assert!(completer.candidates.iter().any(|c| c.contains("file2.txt")));
}
#[test]
fn complete_filename_directory() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("cd {}/sub", path.display());
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1).unwrap();
assert!(result.is_some());
// Should find "subdir"
assert!(completer.candidates.iter().any(|c| c.contains("subdir")));
}
#[test]
fn complete_filename_with_slash() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("ls {}/subdir/", path.display());
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1).unwrap();
// Should complete files in subdir/
if result.is_some() {
assert!(
completer
.candidates
.iter()
.any(|c| c.contains("nested.txt"))
);
}
}
#[test]
fn complete_filename_preserves_trailing_slash() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("cd {}/sub", path.display());
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1).unwrap();
assert!(result.is_some());
let completed = result.unwrap();
// Directory completions should have trailing slash
assert!(completed.ends_with('/'));
}
#[test]
fn complete_filename_relative_path() {
let _temp_dir = setup_local_test_files();
let dir_name = _temp_dir.path().file_name().unwrap().to_str().unwrap();
let mut completer = Completer::new();
let line = format!("cat {}/local", dir_name);
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1).unwrap();
if result.is_some() {
// Should find local1.txt and local2.txt
assert!(completer.candidates.len() >= 2);
}
}
#[test]
fn complete_filename_current_dir() {
let mut completer = Completer::new();
// Complete files in current directory
let line = "cat ".to_string();
let cursor_pos = 4;
let result = completer.complete(line, cursor_pos, 1).unwrap();
// Should find something in current dir (at least Cargo.toml should exist)
if result.is_some() {
assert!(!completer.candidates.is_empty());
}
}
#[test]
fn complete_filename_with_dot_slash() {
let _temp_dir = setup_local_test_files();
let dir_name = _temp_dir.path().file_name().unwrap().to_str().unwrap();
let mut completer = Completer::new();
let line = format!("./{}/local", dir_name);
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1);
// Should preserve the ./
if let Ok(Some(completed)) = result {
assert!(completed.starts_with("./"));
}
}
// ============================================================================
// Completion After '=' Tests
// ============================================================================
#[test]
fn complete_after_equals_assignment() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("FOO={}/fil", path.display());
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1).unwrap();
assert!(result.is_some());
// Should complete filenames after =
assert!(completer.candidates.iter().any(|c| c.contains("file")));
}
#[test]
fn complete_after_equals_option() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("command --output={}/fil", path.display());
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1).unwrap();
assert!(result.is_some());
// Should complete filenames after = in option
assert!(completer.candidates.iter().any(|c| c.contains("file")));
}
#[test]
fn complete_after_equals_empty() {
let mut completer = Completer::new();
let line = "FOO=".to_string();
let cursor_pos = 4;
let result = completer.complete(line, cursor_pos, 1).unwrap();
// Should complete files in current directory when path is empty after =
if result.is_some() {
assert!(!completer.candidates.is_empty());
}
}
// ============================================================================
// Context Detection Tests
// ============================================================================
#[test]
fn context_detection_command_position() {
let completer = Completer::new();
// At the beginning - command context
let (ctx, _) = completer.get_completion_context("ech", 3);
assert!(
ctx.last() == Some(&markers::COMMAND),
"Should be in command context at start"
);
// After whitespace - still command if no command yet
let (ctx, _) = completer.get_completion_context(" ech", 5);
assert!(
ctx.last() == Some(&markers::COMMAND),
"Should be in command context after whitespace"
);
}
#[test]
fn context_detection_argument_position() {
let completer = Completer::new();
// After a complete command - argument context
let (ctx, _) = completer.get_completion_context("echo hello", 10);
assert!(
ctx.last() != Some(&markers::COMMAND),
"Should be in argument context after command"
);
let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11);
assert!(
ctx.last() != Some(&markers::COMMAND),
"Should be in argument context"
);
}
#[test]
fn context_detection_nested_command_sub() {
let completer = Completer::new();
// Inside $() - should be command context
let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11);
assert!(
ctx.last() == Some(&markers::COMMAND),
"Should be in command context inside $()"
);
// After command in $() - argument context
let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17);
assert!(
ctx.last() != Some(&markers::COMMAND),
"Should be in argument context inside $()"
);
}
#[test]
fn context_detection_pipe() {
let completer = Completer::new();
// After pipe - command context
let (ctx, _) = completer.get_completion_context("ls | gre", 8);
assert!(
ctx.last() == Some(&markers::COMMAND),
"Should be in command context after pipe"
);
}
#[test]
fn context_detection_command_sep() {
let completer = Completer::new();
// After semicolon - command context
let (ctx, _) = completer.get_completion_context("echo foo; l", 11);
assert!(
ctx.last() == Some(&markers::COMMAND),
"Should be in command context after semicolon"
);
// After && - command context
let (ctx, _) = completer.get_completion_context("true && l", 9);
assert!(
ctx.last() == Some(&markers::COMMAND),
"Should be in command context after &&"
);
}
#[test]
fn context_detection_variable_substitution() {
let completer = Completer::new();
// $VAR at argument position - VAR_SUB should take priority over ARG
let (ctx, _) = completer.get_completion_context("echo $HOM", 9);
assert_eq!(
ctx.last(),
Some(&markers::VAR_SUB),
"Should be in var_sub context for $HOM"
);
// $VAR at command position - VAR_SUB should take priority over COMMAND
let (ctx, _) = completer.get_completion_context("$HOM", 4);
assert_eq!(
ctx.last(),
Some(&markers::VAR_SUB),
"Should be in var_sub context for bare $HOM"
);
}
#[test]
fn context_detection_variable_in_double_quotes() {
let completer = Completer::new();
// $VAR inside double quotes
let (ctx, _) = completer.get_completion_context("echo \"$HOM", 10);
assert_eq!(
ctx.last(),
Some(&markers::VAR_SUB),
"Should be in var_sub context inside double quotes"
);
}
#[test]
fn context_detection_stack_base_is_null() {
let completer = Completer::new();
// Empty input - only NULL on the stack
let (ctx, _) = completer.get_completion_context("", 0);
assert_eq!(
ctx,
vec![markers::NULL],
"Empty input should only have NULL marker"
);
}
#[test]
fn context_detection_context_start_position() {
let completer = Completer::new();
// Command at start - ctx_start should be 0
let (_, ctx_start) = completer.get_completion_context("ech", 3);
assert_eq!(ctx_start, 0, "Command at start should have ctx_start=0");
// Argument after command - ctx_start should be at arg position
let (_, ctx_start) = completer.get_completion_context("echo hel", 8);
assert_eq!(ctx_start, 5, "Argument ctx_start should point to arg start");
// Variable sub - ctx_start should point to the $
let (_, ctx_start) = completer.get_completion_context("echo $HOM", 9);
assert_eq!(ctx_start, 5, "Var sub ctx_start should point to the $");
}
#[test]
fn context_detection_priority_ordering() {
let completer = Completer::new();
// COMMAND (priority 2) should override ARG (priority 1)
// After a pipe, the next token is a command even though it looks like an arg
let (ctx, _) = completer.get_completion_context("echo foo | gr", 13);
assert_eq!(
ctx.last(),
Some(&markers::COMMAND),
"COMMAND should win over ARG after pipe"
);
// VAR_SUB (priority 3) should override COMMAND (priority 2)
let (ctx, _) = completer.get_completion_context("$PA", 3);
assert_eq!(
ctx.last(),
Some(&markers::VAR_SUB),
"VAR_SUB should win over COMMAND"
);
}
// ============================================================================
// Cycling Behavior Tests
// ============================================================================
#[test]
fn cycle_forward_through_candidates() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("cat {}/file", path.display());
let cursor_pos = line.len();
// First tab
let result1 = completer.complete(line.clone(), cursor_pos, 1).unwrap();
assert!(result1.is_some());
let first_candidate = completer.selected_candidate().unwrap().clone();
// Second tab - should cycle to next
let result2 = completer.complete(line.clone(), cursor_pos, 1).unwrap();
assert!(result2.is_some());
let second_candidate = completer.selected_candidate().unwrap().clone();
// Should be different (if there are multiple candidates)
if completer.candidates.len() > 1 {
assert_ne!(first_candidate, second_candidate);
}
}
#[test]
fn cycle_backward_with_shift_tab() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("cat {}/file", path.display());
let cursor_pos = line.len();
// Forward twice
completer.complete(line.clone(), cursor_pos, 1).unwrap();
let after_first = completer.selected_idx;
completer.complete(line.clone(), cursor_pos, 1).unwrap();
// Backward once (shift-tab = direction -1)
completer.complete(line.clone(), cursor_pos, -1).unwrap();
let after_backward = completer.selected_idx;
// Should be back to first selection
assert_eq!(after_first, after_backward);
}
#[test]
fn cycle_wraps_around() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("cat {}/", path.display());
let cursor_pos = line.len();
// Get all candidates
completer.complete(line.clone(), cursor_pos, 1).unwrap();
let num_candidates = completer.candidates.len();
if num_candidates > 1 {
// Cycle through all and one more
for _ in 0..num_candidates {
completer.complete(line.clone(), cursor_pos, 1).unwrap();
}
// Should wrap back to first (index 0)
assert_eq!(completer.selected_idx, 0);
}
}
#[test]
fn cycle_reset_on_input_change() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line1 = format!("cat {}/file", path.display());
// Complete once
completer.complete(line1.clone(), line1.len(), 1).unwrap();
let candidates_count = completer.candidates.len();
// Change input
let line2 = format!("cat {}/script", path.display());
completer.complete(line2.clone(), line2.len(), 1).unwrap();
// Should have different candidates
// (or at least should have reset the completer state)
assert!(completer.active);
}
#[test]
fn reset_clears_state() {
let mut completer = Completer::new();
// Use a prefix that will definitely have completions
let line = "ec".to_string();
let result = completer.complete(line, 2, 1).unwrap();
// Only check if we got completions
if result.is_some() {
// Should have candidates after completion
assert!(!completer.candidates.is_empty());
completer.reset();
// After reset, state should be cleared
assert!(!completer.active);
assert!(completer.candidates.is_empty());
assert_eq!(completer.selected_idx, 0);
}
}
// ============================================================================
// Edge Cases Tests
// ============================================================================
#[test]
fn complete_empty_input() {
let mut completer = Completer::new();
let line = "".to_string();
let cursor_pos = 0;
let result = completer.complete(line, cursor_pos, 1).unwrap();
// Empty input might return files in current dir or no completion
// Either is valid behavior
}
#[test]
fn complete_whitespace_only() {
let mut completer = Completer::new();
let line = " ".to_string();
let cursor_pos = 3;
let result = completer.complete(line, cursor_pos, 1);
// Should handle gracefully
assert!(result.is_ok());
}
#[test]
fn complete_at_middle_of_word() {
let mut completer = Completer::new();
let line = "echo hello world".to_string();
let cursor_pos = 7; // In the middle of "hello"
let result = completer.complete(line, cursor_pos, 1);
// Should handle cursor in middle of word
assert!(result.is_ok());
}
#[test]
fn complete_with_quotes() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("cat \"{}/fil", path.display());
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1);
// Should handle quoted paths
assert!(result.is_ok());
}
#[test]
fn complete_incomplete_command_substitution() {
let mut completer = Completer::new();
let line = "echo \"$(ech".to_string();
let cursor_pos = 11;
let result = completer.complete(line, cursor_pos, 1);
// Should not crash on incomplete command sub
assert!(result.is_ok());
}
#[test]
fn complete_with_multiple_spaces() {
let mut completer = Completer::new();
let line = "echo hello world".to_string();
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1);
assert!(result.is_ok());
}
#[test]
fn complete_special_characters_in_filename() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path();
// Create files with special characters
fs::write(path.join("file-with-dash.txt"), "").unwrap();
fs::write(path.join("file_with_underscore.txt"), "").unwrap();
let mut completer = Completer::new();
let line = format!("cat {}/file", path.display());
let cursor_pos = line.len();
let result = completer.complete(line, cursor_pos, 1).unwrap();
if result.is_some() {
// Should handle special chars in filenames
assert!(
completer
.candidates
.iter()
.any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))
);
}
}
// ============================================================================
// Integration Tests
// ============================================================================
#[test]
fn complete_full_workflow() {
let temp_dir = setup_test_files();
let path = temp_dir.path();
let mut completer = Completer::new();
let line = format!("cat {}/fil", path.display());
let cursor_pos = line.len();
// Tab 1: Get first completion
let result = completer.complete(line.clone(), cursor_pos, 1).unwrap();
assert!(result.is_some());
let completion1 = result.unwrap();
assert!(completion1.contains("file"));
// Tab 2: Cycle to next
let result = completer.complete(line.clone(), cursor_pos, 1).unwrap();
assert!(result.is_some());
let completion2 = result.unwrap();
// Shift-Tab: Go back
let result = completer.complete(line.clone(), cursor_pos, -1).unwrap();
assert!(result.is_some());
let completion3 = result.unwrap();
// Should be back to first
assert_eq!(completion1, completion3);
}
#[test]
fn complete_mixed_command_and_file() {
let mut completer = Completer::new();
// First part: command completion
let line1 = "ech".to_string();
let result1 = completer.complete(line1, 3, 1).unwrap();
assert!(result1.is_some());
// Reset for new completion
completer.reset();
// Second part: file completion
let line2 = "echo Cargo.tom".to_string();
let result2 = completer.complete(line2, 14, 1).unwrap();
// Both should work
assert!(result1.is_some());
}

View File

@@ -1,174 +0,0 @@
use super::*;
#[test]
fn cmd_not_found() {
let input = "foo";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.next()
.unwrap()
.unwrap();
let err = ShErr::at(ShErrKind::CmdNotFound, token.span, "");
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn unclosed_subsh() {
let input = "(foo";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.nth(1)
.unwrap();
let Err(err) = token else {
panic!("{:?}", token);
};
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn unclosed_dquote() {
let input = "\"foo bar";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.nth(1)
.unwrap();
let Err(err) = token else {
panic!();
};
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn unclosed_squote() {
let input = "'foo bar";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.nth(1)
.unwrap();
let Err(err) = token else {
panic!();
};
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn unclosed_brc_grp() {
let input = "{ foo bar";
let tokens =
LexStream::new(Arc::new(input.into()), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
let Err(err) = tokens else {
panic!("Expected an error, got {:?}", tokens);
};
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn if_no_fi() {
let input = "if foo; then bar;";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() };
let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn if_no_then() {
let input = "if foo; bar; fi";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() };
let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn loop_no_done() {
let input = "while true; do echo foo;";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() };
let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn loop_no_do() {
let input = "while true; echo foo; done";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() };
let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn case_no_esac() {
let input = "case foo in foo) bar;; bar) foo;;";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() };
let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn case_no_in() {
let input = "case foo foo) bar;; bar) foo;; esac";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() };
let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn error_with_notes() {
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
.with_note(Note::new("Execution failed for this reason"))
.with_note(Note::new("Here is how to fix it: blah blah blah"));
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn error_with_notes_and_sub_notes() {
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
.with_note(Note::new("Execution failed for this reason"))
.with_note(Note::new("Here is how to fix it:").with_sub_notes(vec!["blah", "blah", "blah"]));
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}

View File

@@ -1,395 +0,0 @@
use std::collections::HashSet;
use crate::expand::perform_param_expansion;
use crate::prompt::readline::markers;
use crate::state::{VarFlags, VarKind};
use super::*;
#[test]
fn simple_expansion() {
let varsub = "$foo";
write_vars(|v| {
v.set_var(
"foo",
VarKind::Str("this is the value of the variable".into()),
VarFlags::NONE,
)
});
let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.filter(|tk| !matches!(tk.class, TkRule::EOI | TkRule::SOI))
.collect();
let var_tk = tokens.pop().unwrap();
let exp_tk = var_tk.expand().unwrap();
insta::assert_debug_snapshot!(exp_tk.get_words())
}
#[test]
fn unescape_string() {
let string = "echo $foo \\$bar";
let unescaped = unescape_str(string);
insta::assert_snapshot!(unescaped)
}
#[test]
fn expand_alias_simple() {
write_logic(|l| {
l.insert_alias("foo", "echo foo");
let input = String::from("foo");
let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(), "echo foo");
l.clear_aliases();
});
}
#[test]
fn expand_alias_in_if() {
write_logic(|l| {
l.insert_alias("foo", "echo foo");
let input = String::from("if foo; then echo bar; fi");
let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(), "if echo foo; then echo bar; fi");
l.clear_aliases();
});
}
#[test]
fn expand_alias_multiline() {
write_logic(|l| {
l.insert_alias("foo", "echo foo");
l.insert_alias("bar", "echo bar");
let input = String::from(
"
foo
if true; then
bar
fi
",
);
let expected = String::from(
"
echo foo
if true; then
echo bar
fi
",
);
let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result, expected)
});
}
#[test]
fn expand_multiple_aliases() {
write_logic(|l| {
l.insert_alias("foo", "echo foo");
l.insert_alias("bar", "echo bar");
l.insert_alias("biz", "echo biz");
let input = String::from("foo; bar; biz");
let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(), "echo foo; echo bar; echo biz");
});
}
#[test]
fn alias_in_arg_position() {
write_logic(|l| {
l.insert_alias("foo", "echo foo");
let input = String::from("echo foo");
let result = expand_aliases(input.clone(), HashSet::new(), l);
assert_eq!(input, result);
l.clear_aliases();
});
}
#[test]
fn expand_recursive_alias() {
write_logic(|l| {
l.insert_alias("foo", "echo foo");
l.insert_alias("bar", "foo bar");
let input = String::from("bar");
let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(), "echo foo bar");
});
}
#[test]
fn test_infinite_recursive_alias() {
write_logic(|l| {
l.insert_alias("foo", "foo bar");
let input = String::from("foo");
let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(), "foo bar");
l.clear_aliases();
});
}
#[test]
fn param_expansion_defaultunsetornull() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
});
let result = perform_param_expansion("unset:-default").unwrap();
assert_eq!(result, "default");
}
#[test]
fn param_expansion_defaultunset() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
});
let result = perform_param_expansion("unset-default").unwrap();
assert_eq!(result, "default");
}
#[test]
fn param_expansion_setdefaultunsetornull() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
});
let result = perform_param_expansion("unset:=assigned").unwrap();
assert_eq!(result, "assigned");
}
#[test]
fn param_expansion_setdefaultunset() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
});
let result = perform_param_expansion("unset=assigned").unwrap();
assert_eq!(result, "assigned");
}
#[test]
fn param_expansion_altsetnotnull() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
});
let result = perform_param_expansion("set_var:+alt").unwrap();
assert_eq!(result, "alt");
}
#[test]
fn param_expansion_altnotnull() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
});
let result = perform_param_expansion("set_var+alt").unwrap();
assert_eq!(result, "alt");
}
#[test]
fn param_expansion_len() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("#foo").unwrap();
assert_eq!(result, "3");
}
#[test]
fn param_expansion_substr() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo:1").unwrap();
assert_eq!(result, "oo");
}
#[test]
fn param_expansion_substrlen() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo:0:2").unwrap();
assert_eq!(result, "fo");
}
#[test]
fn param_expansion_remshortestprefix() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo#f*").unwrap();
assert_eq!(result, "oo");
}
#[test]
fn param_expansion_remlongestprefix() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo##f*").unwrap();
assert_eq!(result, "");
}
#[test]
fn param_expansion_remshortestsuffix() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo%*o").unwrap();
assert_eq!(result, "fo");
}
#[test]
fn param_expansion_remlongestsuffix() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo%%*o").unwrap();
assert_eq!(result, "");
}
#[test]
fn param_expansion_replacefirstmatch() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo/foo/X").unwrap();
assert_eq!(result, "X");
}
#[test]
fn param_expansion_replaceallmatches() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo//o/X").unwrap();
assert_eq!(result, "fXX");
}
#[test]
fn param_expansion_replaceprefix() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo/#f/X").unwrap();
assert_eq!(result, "Xoo");
}
#[test]
fn param_expansion_replacesuffix() {
write_vars(|v| {
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
});
let result = perform_param_expansion("foo/%o/X").unwrap();
assert_eq!(result, "foX");
}
// ============================================================================
// Double-Quote Escape Tests (POSIX)
// ============================================================================
#[test]
fn dquote_escape_dollar() {
// "\$foo" should strip backslash, produce literal $foo (no expansion)
let result = unescape_str(r#""\$foo""#);
assert!(
!result.contains(markers::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 != markers::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 != markers::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 != markers::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 != markers::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(markers::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(markers::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 != markers::DUB_QUOTE)
.collect();
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
}

View File

@@ -1,51 +0,0 @@
use getopt::{get_opts, get_opts_from_tokens};
use parse::NdRule;
use tests::get_nodes;
use crate::builtin::echo::ECHO_OPTS;
use super::super::*;
#[test]
fn getopt_from_argv() {
let node = get_nodes("echo -n -e foo", |node| {
matches!(node.class, NdRule::Command { .. })
})
.pop()
.unwrap();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
panic!()
};
let (words, opts) = get_opts_from_tokens(argv, &ECHO_OPTS).expect("failed to get opts");
insta::assert_debug_snapshot!(words);
insta::assert_debug_snapshot!(opts)
}
#[test]
fn getopt_simple() {
let raw = "echo -n foo"
.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let (words, opts) = get_opts(raw);
insta::assert_debug_snapshot!(words);
insta::assert_debug_snapshot!(opts);
}
#[test]
fn getopt_multiple_short() {
let raw = "echo -nre foo"
.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let (words, opts) = get_opts(raw);
insta::assert_debug_snapshot!(words);
insta::assert_debug_snapshot!(opts);
}

View File

@@ -1,668 +0,0 @@
use crate::prompt::readline::{
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)
}
/// Helper to find the position of a marker in the annotated string
fn find_marker(annotated: &str, marker: char) -> Option<usize> {
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
}
}
// ============================================================================
// Basic Token-Level Annotation Tests
// ============================================================================
#[test]
fn annotate_simple_command() {
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 ARG marker for "-la"
assert!(has_marker(&annotated, markers::ARG));
// 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);
// 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));
}
#[test]
fn annotate_operator() {
let input = "ls | grep foo";
let annotated = annotate_input(input);
// 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);
}
#[test]
fn annotate_redirect() {
let input = "echo hello > output.txt";
let annotated = annotate_input(input);
// 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);
// 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);
// Should have CMD_SEP marker for semicolon
assert!(has_marker(&annotated, markers::CMD_SEP));
}
// ============================================================================
// Sub-Token Annotation Tests
// ============================================================================
#[test]
fn annotate_variable_simple() {
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));
}
#[test]
fn annotate_variable_braces() {
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));
}
#[test]
fn annotate_double_quoted_string() {
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));
}
#[test]
fn annotate_single_quoted_string() {
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));
}
#[test]
fn annotate_variable_in_string() {
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));
// 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);
// 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);
// 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);
// 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
}
// ============================================================================
// Command Substitution Tests (Flat)
// ============================================================================
#[test]
fn annotate_command_sub_basic() {
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));
}
#[test]
fn annotate_subshell_basic() {
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));
}
#[test]
fn annotate_process_sub_output() {
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));
}
// ============================================================================
// Recursive Annotation Tests
// ============================================================================
#[test]
fn annotate_recursive_command_sub() {
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));
// 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);
// 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);
// 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);
// 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));
}
#[test]
fn annotate_recursive_process_sub() {
let input = "diff <(ls -la)";
let annotated = annotate_input_recursive(input);
// 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));
}
#[test]
fn annotate_recursive_command_sub_in_string() {
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));
}
#[test]
fn annotate_recursive_deeply_nested() {
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();
assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers");
}
// ============================================================================
// Marker Priority/Ordering Tests
// ============================================================================
#[test]
fn marker_priority_var_in_string() {
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
));
}
#[test]
fn marker_priority_arg_vs_string() {
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));
}
#[test]
fn marker_priority_reset_placement() {
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);
}
// ============================================================================
// Highlighter Output Tests
// ============================================================================
#[test]
fn highlighter_produces_ansi_codes() {
let mut highlighter = Highlighter::new();
highlighter.load_input("echo hello", 0);
highlighter.highlight();
let output = highlighter.take();
// 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"));
}
#[test]
fn highlighter_handles_empty_input() {
let mut highlighter = Highlighter::new();
highlighter.load_input("", 0);
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
}
#[test]
fn highlighter_command_validation() {
let mut highlighter = Highlighter::new();
// Valid command (echo exists)
highlighter.load_input("echo test", 0);
highlighter.highlight();
let valid_output = highlighter.take();
// Invalid command (definitely doesn't exist)
highlighter.load_input("xyznotacommand123 test", 0);
highlighter.highlight();
let invalid_output = highlighter.take();
// 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)
}
#[test]
fn highlighter_preserves_text_content() {
let input = "echo hello world";
let mut highlighter = Highlighter::new();
highlighter.load_input(input, 0);
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();
// 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", 0);
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 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""#, 0);
highlighter.highlight();
let output = highlighter.take();
// 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["));
}
#[test]
fn highlighter_reusable() {
let mut highlighter = Highlighter::new();
// First input
highlighter.load_input("echo first", 0);
highlighter.highlight();
let output1 = highlighter.take();
// Second input (reusing same highlighter)
highlighter.load_input("echo second", 0);
highlighter.highlight();
let output2 = highlighter.take();
// 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"));
}
// ============================================================================
// Edge Cases
// ============================================================================
#[test]
fn annotate_unclosed_string() {
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
}
#[test]
fn annotate_unclosed_command_sub() {
let input = "echo $(whoami";
let annotated = annotate_input(input);
// 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);
// 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);
// 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);
// 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);
// 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
}
#[test]
fn annotate_complex_pipeline() {
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 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);
// Should have ASSIGNMENT marker
assert!(has_marker(&annotated, markers::ASSIGNMENT));
// 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));
}
#[test]
fn annotate_redirect_with_fd() {
let input = "command 2>&1";
let annotated = annotate_input(input);
// 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);
// 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);
// Should have REDIRECT marker for <<<
assert!(has_marker(&annotated, markers::REDIRECT));
// 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);
// Should handle unicode gracefully
assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::STRING_SQ));
}
// ============================================================================
// Regression Tests (for bugs we've fixed)
// ============================================================================
#[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);
// 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);
// 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)
}

View File

@@ -1,52 +0,0 @@
use super::*;
#[test]
fn lex_simple() {
let input = "echo hello world";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens)
}
#[test]
fn lex_redir() {
let input = "echo foo > bar.txt";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens)
}
#[test]
fn lex_redir_fds() {
let input = "echo foo 1>&2";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens)
}
#[test]
fn lex_quote_str() {
let input = "echo \"foo bar\" biz baz";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens)
}
#[test]
fn lex_with_keywords() {
let input = "if true; then echo foo; fi";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens)
}
#[test]
fn lex_multiline() {
let input = "echo hello world\necho foo bar\necho boo biz";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens)
}
#[test]
fn lex_case() {
let input = "case $foo in foo) bar;; bar) foo;; biz) baz;; esac";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens)
}

View File

@@ -1,45 +0,0 @@
use std::sync::Arc;
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,
};
use crate::state::{write_logic, write_vars};
pub mod complete;
pub mod error;
pub mod expand;
pub mod getopt;
pub mod highlight;
pub mod lexer;
pub mod parser;
pub mod readline;
pub mod redir;
pub mod script;
pub mod state;
pub mod term;
/// Unsafe to use outside of tests
pub fn get_nodes<F1>(input: &str, filter: F1) -> Vec<Node>
where
F1: Fn(&Node) -> bool,
{
let mut nodes = vec![];
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect::<Vec<_>>();
let mut parsed_nodes = ParseStream::new(tokens)
.map(|nd| nd.unwrap())
.collect::<Vec<_>>();
for node in parsed_nodes.iter_mut() {
node_operation(node, &filter, &mut |node: &mut Node| {
nodes.push(node.clone())
});
}
nodes
}

View File

@@ -1,234 +0,0 @@
use super::*;
#[test]
fn parse_simple() {
let input = "echo hello world";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_pipeline() {
let input = "echo foo | sed s/foo/bar";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_conjunction() {
let input = "echo foo && echo bar";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_conjunction_and_pipeline() {
let input = "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_multiline() {
let input = "
echo hello world
echo foo bar
echo boo biz";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_if_simple() {
let input = "if foo; then echo bar; fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_if_with_elif() {
let input = "if foo; then echo bar; elif bar; then echo foo; fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_if_multiple_elif() {
let input = "if foo; then echo bar; elif bar; then echo foo; elif biz; then echo baz; fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_if_multiline() {
let input = "
if foo; then
echo bar
elif bar; then
echo foo;
elif biz; then
echo baz
fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_loop_simple() {
let input = "while foo; do bar; done";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_loop_until() {
let input = "until foo; do bar; done";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_loop_multiline() {
let input = "
until foo; do
bar
done";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_case_simple() {
let input = "case foo in foo) bar;; bar) foo;; biz) baz;; esac";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_case_multiline() {
let input = "case foo in
foo) bar
;;
bar) foo
;;
biz) baz
;;
esac";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_case_nested() {
let input = "case foo in
foo)
if true; then
while true; do
echo foo
done
fi
;;
bar)
if false; then
until false; do
case foo in
foo)
if true; then
echo foo
fi
;;
bar)
if false; then
echo foo
fi
;;
esac
done
fi
;;
esac";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_cursed() {
let input = "if if if if case foo in foo) if true; then true; fi;; esac; then case foo in foo) until true; do true; done;; esac; fi; then until if case foo in foo) true;; esac; then if true; then true; fi; fi; do until until true; do true; done; do case foo in foo) true;; esac; done; done; fi; then until until case foo in foo) true;; esac; do if true; then true; fi; done; do until true; do true; done; done; fi; then until case foo in foo) case foo in foo) true;; esac;; esac; do if if true; then true; fi; then until true; do true; done; fi; done; elif until until case foo in foo) true;; esac; do if true; then true; fi; done; do case foo in foo) until true; do true; done;; esac; done; then case foo in foo) if case foo in foo) true;; esac; then if true; then true; fi; fi;; esac; else case foo in foo) until until true; do true; done; do case foo in foo) true;; esac; done;; esac; fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
// 15,000 line snapshot file btw
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn test_node_operation() {
let input = String::from("echo hello world; echo foo bar");
let mut check_nodes = vec![];
let tokens: Vec<Tk> = LexStream::new(input.into(), LexFlags::empty())
.map(|tk| tk.unwrap())
.collect();
let nodes = ParseStream::new(tokens).map(|nd| nd.unwrap());
for mut node in nodes {
node_operation(
&mut node,
&|node: &Node| matches!(node.class, NdRule::Command { .. }),
&mut |node: &mut Node| check_nodes.push(node.clone()),
);
}
insta::assert_debug_snapshot!(check_nodes)
}

View File

@@ -1,700 +0,0 @@
use std::collections::VecDeque;
use crate::{
expand::expand_prompt,
libsh::{
error::ShErr,
term::{Style, Styled},
},
prompt::readline::{
Prompt, ShedVi,
history::History,
keys::{KeyCode, KeyEvent, ModKeys},
linebuf::LineBuf,
term::{KeyReader, LineWriter, raw_mode},
vimode::{ViInsert, ViMode, ViNormal},
},
};
use pretty_assertions::assert_eq;
use super::super::*;
#[derive(Default, Debug)]
struct TestReader {
pub bytes: VecDeque<u8>,
}
impl TestReader {
pub fn new() -> Self {
Self::default()
}
pub fn with_initial(mut self, bytes: &[u8]) -> Self {
let bytes = bytes.iter();
self.bytes.extend(bytes);
self
}
pub fn parse_esc_seq_from_bytes(&mut self) -> Option<KeyEvent> {
let mut seq = vec![0x1b];
let b1 = self.bytes.pop_front()?;
seq.push(b1);
match b1 {
b'[' => {
let b2 = self.bytes.pop_front()?;
seq.push(b2);
match b2 {
b'A' => Some(KeyEvent(KeyCode::Up, ModKeys::empty())),
b'B' => Some(KeyEvent(KeyCode::Down, ModKeys::empty())),
b'C' => Some(KeyEvent(KeyCode::Right, ModKeys::empty())),
b'D' => Some(KeyEvent(KeyCode::Left, ModKeys::empty())),
b'1'..=b'9' => {
let mut digits = vec![b2];
while let Some(&b) = self.bytes.front() {
seq.push(b);
self.bytes.pop_front();
if b == b'~' || b == b';' {
break;
} else if b.is_ascii_digit() {
digits.push(b);
} else {
break;
}
}
let key = match digits.as_slice() {
[b'1'] => KeyCode::Home,
[b'3'] => KeyCode::Delete,
[b'4'] => KeyCode::End,
[b'5'] => KeyCode::PageUp,
[b'6'] => KeyCode::PageDown,
[b'7'] => KeyCode::Home, // xterm alternate
[b'8'] => KeyCode::End, // xterm alternate
[b'1', b'5'] => KeyCode::F(5),
[b'1', b'7'] => KeyCode::F(6),
[b'1', b'8'] => KeyCode::F(7),
[b'1', b'9'] => KeyCode::F(8),
[b'2', b'0'] => KeyCode::F(9),
[b'2', b'1'] => KeyCode::F(10),
[b'2', b'3'] => KeyCode::F(11),
[b'2', b'4'] => KeyCode::F(12),
_ => KeyCode::Esc,
};
Some(KeyEvent(key, ModKeys::empty()))
}
_ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())),
}
}
b'O' => {
let b2 = self.bytes.pop_front()?;
seq.push(b2);
let key = match b2 {
b'P' => KeyCode::F(1),
b'Q' => KeyCode::F(2),
b'R' => KeyCode::F(3),
b'S' => KeyCode::F(4),
_ => KeyCode::Esc,
};
Some(KeyEvent(key, ModKeys::empty()))
}
_ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())),
}
}
}
impl KeyReader for TestReader {
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> {
use core::str;
let mut collected = Vec::with_capacity(4);
loop {
let byte = self.bytes.pop_front();
if byte.is_none() {
return Ok(None);
}
let byte = byte.unwrap();
collected.push(byte);
// If it's an escape sequence, delegate
if collected[0] == 0x1b && collected.len() == 1 {
if let Some(&_next @ (b'[' | b'0')) = self.bytes.front() {
println!("found escape seq");
let seq = self.parse_esc_seq_from_bytes();
println!("{seq:?}");
return Ok(seq);
}
}
// Try parse as valid UTF-8
if let Ok(s) = str::from_utf8(&collected) {
return Ok(Some(KeyEvent::new(s, ModKeys::empty())));
}
if collected.len() >= 4 {
break;
}
}
Ok(None)
}
}
pub struct TestWriter {}
impl TestWriter {
pub fn new() -> Self {
Self {}
}
}
impl LineWriter for TestWriter {
fn clear_rows(&mut self, _layout: &prompt::readline::term::Layout) -> libsh::error::ShResult<()> {
Ok(())
}
fn redraw(
&mut self,
_prompt: &str,
_line: &str,
_new_layout: &prompt::readline::term::Layout,
) -> libsh::error::ShResult<()> {
Ok(())
}
fn flush_write(&mut self, _buf: &str) -> libsh::error::ShResult<()> {
Ok(())
}
fn send_bell(&mut self) -> ShResult<()> {
Ok(())
}
}
// NOTE: ShedVi structure has changed significantly and readline() method no
// longer exists These test helpers are disabled until they can be properly
// updated
/*
impl ShedVi {
pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self {
Self {
reader: Box::new(TestReader::new().with_initial(input.as_bytes())),
writer: Box::new(TestWriter::new()),
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
mode: Box::new(ViInsert::new()),
old_layout: None,
repeat_action: None,
repeat_motion: None,
history: History::new().unwrap(),
editor: LineBuf::new().with_initial(initial, 0),
}
}
}
fn shedvi_test(input: &str, initial: &str) -> String {
let mut shedvi = ShedVi::new_test(None, input, initial);
let raw_mode = raw_mode();
let line = shedvi.readline().unwrap();
std::mem::drop(raw_mode);
line
}
*/
fn normal_cmd(cmd: &str, buf: &str, cursor: usize) -> (String, usize) {
let cmd = ViNormal::new().cmds_from_raw(cmd).pop().unwrap();
let mut buf = LineBuf::new().with_initial(buf, cursor);
buf.exec_cmd(cmd).unwrap();
(buf.as_str().to_string(), buf.cursor.get())
}
#[test]
fn vimode_insert_cmds() {
let raw = "abcdefghijklmnopqrstuvwxyz1234567890-=[];'<>/\\x1b";
let mut mode = ViInsert::new();
let cmds = mode.cmds_from_raw(raw);
insta::assert_debug_snapshot!(cmds)
}
#[test]
fn vimode_normal_cmds() {
let raw = "d2wg?5b2P5x";
let mut mode = ViNormal::new();
let cmds = mode.cmds_from_raw(raw);
insta::assert_debug_snapshot!(cmds)
}
#[test]
fn linebuf_empty_linebuf() {
let mut buf = LineBuf::new();
assert_eq!(buf.as_str(), "");
buf.update_graphemes_lazy();
assert_eq!(buf.grapheme_indices(), &[]);
assert!(buf.slice(0..0).is_none());
}
#[test]
fn linebuf_ascii_content() {
let mut buf = LineBuf::new().with_initial("hello", 0);
buf.update_graphemes_lazy();
assert_eq!(buf.grapheme_indices(), &[0, 1, 2, 3, 4]);
assert_eq!(buf.grapheme_at(0), Some("h"));
assert_eq!(buf.grapheme_at(4), Some("o"));
assert_eq!(buf.slice(1..4), Some("ell"));
assert_eq!(buf.slice_to(2), Some("he"));
assert_eq!(buf.slice_from(2), Some("llo"));
}
#[test]
fn expand_default_prompt() {
let prompt = expand_prompt(
"\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "
.into(),
)
.unwrap();
insta::assert_debug_snapshot!(prompt)
}
#[test]
fn linebuf_unicode_graphemes() {
let mut buf = LineBuf::new().with_initial("a🇺🇸b́c", 0);
buf.update_graphemes_lazy();
let indices = buf.grapheme_indices();
assert_eq!(indices.len(), 4); // 4 graphemes + 1 end marker
assert_eq!(buf.grapheme_at(0), Some("a"));
assert_eq!(buf.grapheme_at(1), Some("🇺🇸"));
assert_eq!(buf.grapheme_at(2), Some("")); // b + combining accent
assert_eq!(buf.grapheme_at(3), Some("c"));
assert_eq!(buf.grapheme_at(4), None); // out of bounds
assert_eq!(buf.slice(0..2), Some("a🇺🇸"));
assert_eq!(buf.slice(1..3), Some("🇺🇸b́"));
assert_eq!(buf.slice(2..4), Some("b́c"));
}
#[test]
fn linebuf_slice_to_from_cursor() {
let mut buf = LineBuf::new().with_initial("abçd", 2);
buf.update_graphemes_lazy();
assert_eq!(buf.slice_to_cursor(), Some("ab"));
assert_eq!(buf.slice_from_cursor(), Some("çd"));
}
#[test]
fn linebuf_out_of_bounds_slices() {
let mut buf = LineBuf::new().with_initial("test", 0);
buf.update_graphemes_lazy();
assert_eq!(buf.grapheme_at(5), None); // out of bounds
assert_eq!(buf.slice(2..5), None); // end out of bounds
assert_eq!(buf.slice(4..4), None); // valid but empty
}
#[test]
fn linebuf_this_line() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start, end) = buf.this_line();
assert_eq!(buf.slice(start..end), Some("This is the third line\n"))
}
#[test]
fn linebuf_prev_line() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start, end) = buf.nth_prev_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the second line\n"))
}
#[test]
fn linebuf_prev_line_first_line_is_empty() {
let initial = "\nThis is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 36);
let (start, end) = buf.nth_prev_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the first line\n"))
}
#[test]
fn linebuf_next_line() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start, end) = buf.nth_next_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the fourth line"))
}
#[test]
fn linebuf_next_line_last_line_is_empty() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start, end) = buf.nth_next_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the fourth line\n"))
}
#[test]
fn linebuf_next_line_several_trailing_newlines() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n\n\n\n";
let mut buf = LineBuf::new().with_initial(initial, 81);
let (start, end) = buf.nth_next_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("\n"))
}
#[test]
fn linebuf_next_line_only_newlines() {
let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
let mut buf = LineBuf::new().with_initial(initial, 7);
let (start, end) = buf.nth_next_line(1).unwrap();
assert_eq!(start, 8);
assert_eq!(buf.slice(start..end), Some("\n"))
}
#[test]
fn linebuf_prev_line_only_newlines() {
let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
let mut buf = LineBuf::new().with_initial(initial, 7);
let (start, end) = buf.nth_prev_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("\n"));
assert_eq!(start, 6);
}
#[test]
fn linebuf_cursor_motion() {
let mut buf =
LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶", 0);
buf.update_graphemes_lazy();
let total = buf.grapheme_indices.as_ref().unwrap().len();
for i in 0..total {
buf.cursor.set(i);
let expected_to = buf
.buffer
.get(..buf.grapheme_indices_owned()[i])
.unwrap_or("")
.to_string();
let expected_from = if i + 1 < total {
buf
.buffer
.get(buf.grapheme_indices_owned()[i]..)
.unwrap_or("")
.to_string()
} else {
// last grapheme, ends at buffer end
buf
.buffer
.get(buf.grapheme_indices_owned()[i]..)
.unwrap_or("")
.to_string()
};
let expected_at = {
let start = buf.grapheme_indices_owned()[i];
let end = buf
.grapheme_indices_owned()
.get(i + 1)
.copied()
.unwrap_or(buf.buffer.len());
buf.buffer.get(start..end).map(|slice| slice.to_string())
};
assert_eq!(
buf.slice_to_cursor(),
Some(expected_to.as_str()),
"Failed at cursor position {i}: slice_to_cursor"
);
assert_eq!(
buf.slice_from_cursor(),
Some(expected_from.as_str()),
"Failed at cursor position {i}: slice_from_cursor"
);
assert_eq!(
buf.grapheme_at(i).map(|slice| slice.to_string()),
expected_at,
"Failed at cursor position {i}: grapheme_at"
);
}
}
#[test]
fn editor_delete_word() {
assert_eq!(
normal_cmd("dw", "The quick brown fox jumps over the lazy dog", 16),
("The quick brown jumps over the lazy dog".into(), 16)
);
}
#[test]
fn editor_delete_backwards() {
assert_eq!(
normal_cmd("2db", "The quick brown fox jumps over the lazy dog", 16),
("The fox jumps over the lazy dog".into(), 4)
);
}
#[test]
fn editor_rot13_five_words_backwards() {
assert_eq!(
normal_cmd("g?5b", "The quick brown fox jumps over the lazy dog", 31),
("The dhvpx oebja sbk whzcf bire the lazy dog".into(), 4)
);
}
#[test]
fn editor_delete_word_on_whitespace() {
assert_eq!(
normal_cmd("dw", "The quick brown fox", 10), //on the whitespace between "quick" and "brown"
("The quick brown fox".into(), 10)
);
}
#[test]
fn editor_delete_5_words() {
assert_eq!(
normal_cmd("5dw", "The quick brown fox jumps over the lazy dog", 16,),
("The quick brown dog".into(), 16)
);
}
#[test]
fn editor_delete_end_includes_last() {
assert_eq!(
normal_cmd("de", "The quick brown fox::::jumps over the lazy dog", 16),
("The quick brown ::::jumps over the lazy dog".into(), 16)
);
}
#[test]
fn editor_delete_end_unicode_word() {
assert_eq!(
normal_cmd("de", "naïve café world", 0),
(" café world".into(), 0)
);
}
#[test]
fn editor_inplace_edit_cursor_position() {
assert_eq!(normal_cmd("5~", "foobar", 0), ("FOOBAr".into(), 4));
assert_eq!(normal_cmd("5rg", "foobar", 0), ("gggggr".into(), 4));
}
#[test]
fn editor_insert_mode_not_clamped() {
assert_eq!(normal_cmd("a", "foobar", 5), ("foobar".into(), 6))
}
#[test]
fn editor_overshooting_motions() {
assert_eq!(normal_cmd("5dw", "foo bar", 0), ("".into(), 0));
assert_eq!(normal_cmd("3db", "foo bar", 0), ("foo bar".into(), 0));
assert_eq!(normal_cmd("3dj", "foo bar", 0), ("foo bar".into(), 0));
assert_eq!(normal_cmd("3dk", "foo bar", 0), ("foo bar".into(), 0));
}
#[test]
fn editor_textobj_quoted() {
assert_eq!(
normal_cmd("di\"", "this buffer has \"some \\\"quoted\" text", 0),
("this buffer has \"\" text".into(), 17)
);
assert_eq!(
normal_cmd("da\"", "this buffer has \"some \\\"quoted\" text", 0),
("this buffer has text".into(), 16)
);
assert_eq!(
normal_cmd("di'", "this buffer has 'some \\'quoted' text", 0),
("this buffer has '' text".into(), 17)
);
assert_eq!(
normal_cmd("da'", "this buffer has 'some \\'quoted' text", 0),
("this buffer has text".into(), 16)
);
assert_eq!(
normal_cmd("di`", "this buffer has `some \\`quoted` text", 0),
("this buffer has `` text".into(), 17)
);
assert_eq!(
normal_cmd("da`", "this buffer has `some \\`quoted` text", 0),
("this buffer has text".into(), 16)
);
}
#[test]
fn editor_textobj_delimited() {
assert_eq!(
normal_cmd(
"di)",
"this buffer has (some \\(\\)(inner) \\(\\)delimited) text",
0
),
("this buffer has () text".into(), 17)
);
assert_eq!(
normal_cmd(
"da)",
"this buffer has (some \\(\\)(inner) \\(\\)delimited) text",
0
),
("this buffer has text".into(), 16)
);
assert_eq!(
normal_cmd(
"di]",
"this buffer has [some \\[\\][inner] \\[\\]delimited] text",
0
),
("this buffer has [] text".into(), 17)
);
assert_eq!(
normal_cmd(
"da]",
"this buffer has [some \\[\\][inner] \\[\\]delimited] text",
0
),
("this buffer has text".into(), 16)
);
assert_eq!(
normal_cmd(
"di}",
"this buffer has {some \\{\\}{inner} \\{\\}delimited} text",
0
),
("this buffer has {} text".into(), 17)
);
assert_eq!(
normal_cmd(
"da}",
"this buffer has {some \\{\\}{inner} \\{\\}delimited} text",
0
),
("this buffer has text".into(), 16)
);
assert_eq!(
normal_cmd(
"di>",
"this buffer has <some \\<\\><inner> \\<\\>delimited> text",
0
),
("this buffer has <> text".into(), 17)
);
assert_eq!(
normal_cmd(
"da>",
"this buffer has <some \\<\\><inner> \\<\\>delimited> text",
0
),
("this buffer has text".into(), 16)
);
}
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 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 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.";
#[test]
fn editor_delete_line_up() {
assert_eq!(normal_cmd(
"dk",
LOREM_IPSUM,
237),
("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\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.".into(), 129,)
)
}
#[test]
fn editor_insert_at_line_start() {
// I should move cursor to position 0 when line starts with non-whitespace
assert_eq!(normal_cmd("I", "hello world", 5), ("hello world".into(), 0));
// I should skip leading whitespace
assert_eq!(
normal_cmd("I", " hello world", 8),
(" hello world".into(), 2)
);
// I should move to the first non-whitespace on the current line in a multiline
// buffer
assert_eq!(
normal_cmd("I", "first line\nsecond line", 14),
("first line\nsecond line".into(), 11)
);
// I should land on position 0 when cursor is already at 0
assert_eq!(normal_cmd("I", "hello", 0), ("hello".into(), 0));
}
#[test]
fn editor_f_char_from_position_zero() {
// f<char> at position 0 should skip the cursor and find the next occurrence
// Regression: previously at pos 0, f would match the char under the cursor
// itself
assert_eq!(
normal_cmd("fa", "abcaef", 0),
("abcaef".into(), 3) // should find second 'a', not the 'a' at position 0
);
// f<char> from position 0 finding a char that only appears later
assert_eq!(
normal_cmd("fo", "hello world", 0),
("hello world".into(), 4)
);
// f<char> from middle of buffer
assert_eq!(
normal_cmd("fd", "hello world", 5),
("hello world".into(), 10)
);
}
// NOTE: These tests disabled because shedvi_test() helper is commented out
/*
#[test]
fn shedvi_test_simple() {
assert_eq!(shedvi_test("foo bar\x1bbdw\r", ""), "foo ")
}
#[test]
fn shedvi_test_mode_change() {
assert_eq!(
shedvi_test("foo bar biz buzz\x1bbbb2cwbiz buzz bar\r", ""),
"foo biz buzz bar buzz"
)
}
#[test]
fn shedvi_test_lorem_ipsum_1() {
assert_eq!(shedvi_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."
)
}
#[test]
fn shedvi_test_lorem_ipsum_undo() {
assert_eq!(
shedvi_test(
"\x1bwwwwwwwwainserting some characters now...\x1bu\r",
LOREM_IPSUM
),
LOREM_IPSUM
)
}
#[test]
fn shedvi_test_lorem_ipsum_ctrl_w() {
assert_eq!(shedvi_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."
)
}
*/

View File

@@ -1,428 +0,0 @@
use std::sync::Arc;
use crate::parse::{
NdRule, Node, ParseStream, Redir, RedirType,
lex::{LexFlags, LexStream},
};
use crate::procio::{IoFrame, IoMode, IoStack};
// ============================================================================
// Parser Tests - Redirection Syntax
// ============================================================================
fn parse_command(input: &str) -> Node {
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<_>>();
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
),
}
}
#[test]
fn parse_output_redirect() {
let node = parse_command("echo hello > output.txt");
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, .. }));
}
#[test]
fn parse_append_redirect() {
let node = parse_command("echo hello >> output.txt");
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, .. }));
}
#[test]
fn parse_input_redirect() {
let node = parse_command("cat < input.txt");
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, .. }));
}
#[test]
fn parse_stderr_redirect() {
let node = parse_command("ls 2> errors.txt");
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, .. }));
}
#[test]
fn parse_stderr_to_stdout() {
let node = parse_command("ls 2>&1");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
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");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
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");
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, .. }
));
// 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, .. }
));
}
#[test]
fn parse_custom_fd_redirect() {
let node = parse_command("echo test 3> fd3.txt");
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, .. }));
}
#[test]
fn parse_custom_fd_dup() {
let node = parse_command("cmd 3>&4");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(
redir.io_mode,
IoMode::Fd {
tgt_fd: 3,
src_fd: 4
}
));
}
#[test]
fn parse_heredoc() {
let node = parse_command("cat << EOF");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::HereDoc));
}
#[test]
fn parse_herestring() {
let node = parse_command("cat <<< 'hello world'");
assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::HereString));
}
#[test]
fn parse_redirect_with_no_space() {
let node = parse_command("echo hello >output.txt");
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");
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
}
));
// Second redirect: > file.txt
assert!(matches!(node.redirs[1].class, RedirType::Output));
assert!(matches!(
node.redirs[1].io_mode,
IoMode::File { tgt_fd: 1, .. }
));
}
// ============================================================================
// IoStack Tests - Data Structure Logic
// ============================================================================
#[test]
fn 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");
}
#[test]
fn iostack_push_pop_frame() {
let mut stack = IoStack::new();
// 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);
}
#[test]
fn iostack_never_empties() {
let mut stack = IoStack::new();
// 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);
// 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 redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
stack.push_to_frame(redir);
assert_eq!(stack.curr_frame().len(), 1);
}
#[test]
fn iostack_append_to_frame() {
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),
];
stack.append_to_frame(redirs);
assert_eq!(stack.curr_frame().len(), 2);
}
#[test]
fn iostack_frame_isolation() {
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);
// 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);
// 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);
}
#[test]
fn iostack_flatten() {
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);
// 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);
assert_eq!(stack.len(), 3);
// Flatten
stack.flatten();
// 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);
}
#[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 frame = IoFrame::from_redirs(redirs);
assert_eq!(frame.len(), 2);
}
#[test]
fn ioframe_push() {
let mut frame = IoFrame::new();
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
frame.push(redir);
assert_eq!(frame.len(), 1);
}
// ============================================================================
// IoMode Tests - Construction Logic
// ============================================================================
#[test]
fn iomode_fd_construction() {
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"),
}
}
#[test]
fn iomode_tgt_fd() {
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);
}
#[test]
fn iomode_src_fd() {
let fd_mode = IoMode::fd(2, 1);
assert_eq!(fd_mode.src_fd(), 1);
}

View File

@@ -1,51 +0,0 @@
use std::process::{self, Output};
use pretty_assertions::assert_eq;
use super::super::*;
fn get_script_output(name: &str, args: &[&str]) -> Output {
// Resolve the path to the shed binary.
// Do not question me.
let mut shed_path = env::current_exe().expect("Failed to get test executable"); // The path to the test executable
shed_path.pop(); // Hocus pocus
shed_path.pop();
shed_path.push("shed"); // Abra Kadabra
if !shed_path.is_file() {
shed_path.pop();
shed_path.pop();
shed_path.push("release");
shed_path.push("shed");
}
if !shed_path.is_file() {
panic!("where the hell is the binary")
}
process::Command::new(shed_path) // Alakazam
.arg(name)
.args(args)
.output()
.expect("Failed to run script")
}
#[test]
fn script_hello_world() {
let output = get_script_output("./test_scripts/hello.sh", &[]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "Hello, World!")
}
#[test]
fn script_cmdsub() {
let output = get_script_output("./test_scripts/cmdsub.sh", &[]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "foo Hello bar")
}
#[test]
fn script_multiline() {
let output = get_script_output("./test_scripts/multiline.sh", &[]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "foo\nbar\nbiz\nbuzz")
}

View File

@@ -1,895 +0,0 @@
use crate::state::{LogTab, MetaTab, ScopeStack, ShellParam, VarFlags, VarKind, VarTab};
use std::path::PathBuf;
// ============================================================================
// ScopeStack Tests - Variable Scoping
// ============================================================================
#[test]
fn 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
}
#[test]
fn scopestack_descend_ascend() {
let mut stack = ScopeStack::new();
// Set a global variable
stack.set_var("GLOBAL", VarKind::Str("value1".into()), VarFlags::NONE);
assert_eq!(stack.get_var("GLOBAL"), "value1");
// Descend into a new scope
stack.descend(None);
// Global should still be visible
assert_eq!(stack.get_var("GLOBAL"), "value1");
// Set a local variable
stack.set_var("LOCAL", VarKind::Str("value2".into()), VarFlags::LOCAL);
assert_eq!(stack.get_var("LOCAL"), "value2");
// Ascend back to global scope
stack.ascend();
// Global should still exist
assert_eq!(stack.get_var("GLOBAL"), "value1");
// Local should no longer be visible
assert_eq!(stack.get_var("LOCAL"), "");
}
#[test]
fn scopestack_variable_shadowing() {
let mut stack = ScopeStack::new();
// Set global variable
stack.set_var("VAR", VarKind::Str("global".into()), VarFlags::NONE);
assert_eq!(stack.get_var("VAR"), "global");
// Descend into local scope
stack.descend(None);
// Set local variable with same name
stack.set_var("VAR", VarKind::Str("local".into()), VarFlags::LOCAL);
assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global");
// Ascend back
stack.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();
// Descend into a local scope
stack.descend(None);
// Set with LOCAL flag - should go in current scope
stack.set_var("LOCAL_VAR", VarKind::Str("local".into()), VarFlags::LOCAL);
// Set without LOCAL flag - should go in global scope
stack.set_var("GLOBAL_VAR", VarKind::Str("global".into()), VarFlags::NONE);
// 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();
// 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();
stack.set_var("LEVEL0", VarKind::Str("global".into()), VarFlags::NONE);
// Level 1
stack.descend(None);
stack.set_var("LEVEL1", VarKind::Str("first".into()), VarFlags::LOCAL);
// Level 2
stack.descend(None);
stack.set_var("LEVEL2", VarKind::Str("second".into()), 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");
// 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"), "");
}
#[test]
fn scopestack_cannot_ascend_past_global() {
let mut stack = ScopeStack::new();
stack.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE);
// 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");
}
#[test]
fn scopestack_descend_with_args() {
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));
// 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"
);
// Ascend back
stack.ascend();
// 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();
// 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");
// 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");
// Modify global parameter from local scope
stack.set_param(ShellParam::Status, "1");
assert_eq!(stack.get_param(ShellParam::Status), "1");
// Ascend
stack.ascend();
// Global parameter should retain modified value
assert_eq!(stack.get_param(ShellParam::Status), "1");
}
#[test]
fn scopestack_unset_var() {
let mut stack = ScopeStack::new();
stack.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE);
assert_eq!(stack.get_var("VAR"), "value");
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();
// Set global
stack.set_var("VAR", VarKind::Str("global".into()), VarFlags::NONE);
// Descend and shadow
stack.descend(None);
stack.set_var("VAR", VarKind::Str("local".into()), 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");
}
#[test]
fn scopestack_export_var() {
let mut stack = ScopeStack::new();
stack.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE);
// Export the variable
stack.export_var("VAR");
// 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();
assert!(!stack.var_exists("NONEXISTENT"));
stack.set_var("EXISTS", VarKind::Str("yes".into()), VarFlags::NONE);
assert!(stack.var_exists("EXISTS"));
stack.descend(None);
assert!(
stack.var_exists("EXISTS"),
"Global var should be visible in local scope"
);
stack.set_var("LOCAL", VarKind::Str("yes".into()), VarFlags::LOCAL);
assert!(stack.var_exists("LOCAL"));
stack.ascend();
assert!(
!stack.var_exists("LOCAL"),
"Local var should not exist after ascend"
);
}
#[test]
fn scopestack_flatten_vars() {
let mut stack = ScopeStack::new();
stack.set_var("GLOBAL1", VarKind::Str("g1".into()), VarFlags::NONE);
stack.set_var("GLOBAL2", VarKind::Str("g2".into()), VarFlags::NONE);
stack.descend(None);
stack.set_var("LOCAL1", VarKind::Str("l1".into()), VarFlags::LOCAL);
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"));
}
#[test]
fn scopestack_local_var_mutation() {
let mut stack = ScopeStack::new();
// Descend into function scope
stack.descend(None);
// `local foo="biz"` — create a local variable with initial value
stack.set_var("foo", VarKind::Str("biz".into()), VarFlags::LOCAL);
assert_eq!(stack.get_var("foo"), "biz");
// `foo="bar"` — reassign without LOCAL flag (plain assignment)
stack.set_var("foo", VarKind::Str("bar".into()), VarFlags::NONE);
assert_eq!(
stack.get_var("foo"),
"bar",
"Local var should be mutated in place"
);
// Ascend back to global
stack.ascend();
// foo should not exist in global scope
assert_eq!(
stack.get_var("foo"),
"",
"Local var should not leak to global scope"
);
}
#[test]
fn scopestack_local_var_uninitialized() {
let mut stack = ScopeStack::new();
// Descend into function scope
stack.descend(None);
// `local foo` — declare without a value
stack.set_var("foo", VarKind::Str("".into()), VarFlags::LOCAL);
assert_eq!(stack.get_var("foo"), "");
// `foo="bar"` — assign a value later
stack.set_var("foo", VarKind::Str("bar".into()), VarFlags::NONE);
assert_eq!(
stack.get_var("foo"),
"bar",
"Uninitialized local should be assignable"
);
// Ascend back to global
stack.ascend();
// foo should not exist in global scope
assert_eq!(
stack.get_var("foo"),
"",
"Local var should not leak to global scope"
);
}
// ============================================================================
// LogTab Tests - Functions and Aliases
// ============================================================================
#[test]
fn logtab_new() {
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();
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();
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()));
}
#[test]
fn logtab_remove_alias() {
let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la");
assert!(logtab.get_alias("ll").is_some());
logtab.remove_alias("ll");
assert!(logtab.get_alias("ll").is_none());
}
#[test]
fn logtab_clear_aliases() {
let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la");
logtab.insert_alias("la", "ls -A");
logtab.insert_alias("l", "ls -CF");
assert_eq!(logtab.aliases().len(), 3);
logtab.clear_aliases();
assert_eq!(logtab.aliases().len(), 0);
}
#[test]
fn logtab_multiple_aliases() {
let mut logtab = LogTab::new();
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())
);
}
// 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());
}
// ============================================================================
// VarTab Tests - Variable Storage
// ============================================================================
#[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());
}
#[test]
fn vartab_set_get_var() {
let mut vartab = VarTab::new();
vartab.set_var("TEST", VarKind::Str("value".into()), VarFlags::NONE);
assert_eq!(vartab.get_var("TEST"), "value");
}
#[test]
fn vartab_overwrite_var() {
let mut vartab = VarTab::new();
vartab.set_var("VAR", VarKind::Str("value1".into()), VarFlags::NONE);
assert_eq!(vartab.get_var("VAR"), "value1");
vartab.set_var("VAR", VarKind::Str("value2".into()), VarFlags::NONE);
assert_eq!(vartab.get_var("VAR"), "value2");
}
#[test]
fn vartab_var_exists() {
let mut vartab = VarTab::new();
assert!(!vartab.var_exists("TEST"));
vartab.set_var("TEST", VarKind::Str("value".into()), VarFlags::NONE);
assert!(vartab.var_exists("TEST"));
}
#[test]
fn vartab_unset_var() {
let mut vartab = VarTab::new();
vartab.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE);
assert!(vartab.var_exists("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();
vartab.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE);
vartab.export_var("VAR");
// Variable should still be accessible
assert_eq!(vartab.get_var("VAR"), "value");
}
#[test]
fn vartab_positional_params() {
let mut vartab = VarTab::new();
// 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
// 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"
);
// 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();
// 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());
// 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());
// Should have one fewer arg
assert_eq!(vartab.sh_argv().len(), initial_len - 1);
}
// ============================================================================
// VarFlags Tests
// ============================================================================
#[test]
fn varflags_none() {
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));
}
#[test]
fn 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));
}
#[test]
fn varflags_readonly() {
let flags = VarFlags::READONLY;
assert!(flags.contains(VarFlags::READONLY));
assert!(!flags.contains(VarFlags::EXPORT));
}
// ============================================================================
// ShellParam Tests
// ============================================================================
#[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::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
));
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)"),
}
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");
}
// ============================================================================
// MetaTab Directory Stack Tests
// ============================================================================
#[test]
fn dirstack_push_pop() {
let mut meta = MetaTab::new();
meta.push_dir(PathBuf::from("/tmp"));
meta.push_dir(PathBuf::from("/var"));
// push_front means /var is on top, /tmp is below
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/var")));
let popped = meta.pop_dir();
assert_eq!(popped, Some(PathBuf::from("/var")));
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/tmp")));
let popped = meta.pop_dir();
assert_eq!(popped, Some(PathBuf::from("/tmp")));
assert_eq!(meta.pop_dir(), None);
}
#[test]
fn dirstack_empty() {
let mut meta = MetaTab::new();
assert_eq!(meta.dir_stack_top(), None);
assert_eq!(meta.pop_dir(), None);
assert!(meta.dirs().is_empty());
}
#[test]
fn dirstack_rotate_fwd() {
let mut meta = MetaTab::new();
// Build stack: front=[A, B, C, D]=back
meta.dirs_mut().push_back(PathBuf::from("/a"));
meta.dirs_mut().push_back(PathBuf::from("/b"));
meta.dirs_mut().push_back(PathBuf::from("/c"));
meta.dirs_mut().push_back(PathBuf::from("/d"));
// rotate_left(1): [B, C, D, A]
meta.rotate_dirs_fwd(1);
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/b")));
assert_eq!(meta.dirs().back(), Some(&PathBuf::from("/a")));
}
#[test]
fn dirstack_rotate_bkwd() {
let mut meta = MetaTab::new();
// Build stack: front=[A, B, C, D]=back
meta.dirs_mut().push_back(PathBuf::from("/a"));
meta.dirs_mut().push_back(PathBuf::from("/b"));
meta.dirs_mut().push_back(PathBuf::from("/c"));
meta.dirs_mut().push_back(PathBuf::from("/d"));
// rotate_right(1): [D, A, B, C]
meta.rotate_dirs_bkwd(1);
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/d")));
assert_eq!(meta.dirs().back(), Some(&PathBuf::from("/c")));
}
#[test]
fn dirstack_rotate_zero_is_noop() {
let mut meta = MetaTab::new();
meta.dirs_mut().push_back(PathBuf::from("/a"));
meta.dirs_mut().push_back(PathBuf::from("/b"));
meta.dirs_mut().push_back(PathBuf::from("/c"));
meta.rotate_dirs_fwd(0);
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/a")));
meta.rotate_dirs_bkwd(0);
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/a")));
}
#[test]
fn dirstack_pushd_rotation_with_cwd() {
// Simulates what pushd +N does: insert cwd, rotate, pop new top
let mut meta = MetaTab::new();
// Stored stack: [/tmp, /var, /etc]
meta.push_dir(PathBuf::from("/etc"));
meta.push_dir(PathBuf::from("/var"));
meta.push_dir(PathBuf::from("/tmp"));
// pushd +2 with cwd=/home:
// push_front(cwd): [/home, /tmp, /var, /etc]
// rotate_left(2): [/var, /etc, /home, /tmp]
// pop_front(): /var = new cwd
let cwd = PathBuf::from("/home");
let dirs = meta.dirs_mut();
dirs.push_front(cwd);
dirs.rotate_left(2);
let new_cwd = dirs.pop_front();
assert_eq!(new_cwd, Some(PathBuf::from("/var")));
let remaining: Vec<_> = meta.dirs().iter().collect();
assert_eq!(
remaining,
vec![
&PathBuf::from("/etc"),
&PathBuf::from("/home"),
&PathBuf::from("/tmp"),
]
);
}
#[test]
fn dirstack_pushd_minus_zero_with_cwd() {
// pushd -0: bring bottom to top
let mut meta = MetaTab::new();
// Stored stack: [/tmp, /var, /etc]
meta.push_dir(PathBuf::from("/etc"));
meta.push_dir(PathBuf::from("/var"));
meta.push_dir(PathBuf::from("/tmp"));
// pushd -0 with cwd=/home:
// push_front(cwd): [/home, /tmp, /var, /etc]
// rotate_right(0+1=1): [/etc, /home, /tmp, /var]
// pop_front(): /etc = new cwd
let cwd = PathBuf::from("/home");
let dirs = meta.dirs_mut();
dirs.push_front(cwd);
dirs.rotate_right(1);
let new_cwd = dirs.pop_front();
assert_eq!(new_cwd, Some(PathBuf::from("/etc")));
}
#[test]
fn dirstack_pushd_plus_zero_noop() {
// pushd +0: should be a no-op (cwd stays the same)
let mut meta = MetaTab::new();
meta.push_dir(PathBuf::from("/etc"));
meta.push_dir(PathBuf::from("/var"));
meta.push_dir(PathBuf::from("/tmp"));
// pushd +0 with cwd=/home:
// push_front(cwd): [/home, /tmp, /var, /etc]
// rotate_left(0): no-op
// pop_front(): /home = cwd unchanged
let cwd = PathBuf::from("/home");
let dirs = meta.dirs_mut();
dirs.push_front(cwd.clone());
dirs.rotate_left(0);
let new_cwd = dirs.pop_front();
assert_eq!(new_cwd, Some(PathBuf::from("/home")));
}
#[test]
fn dirstack_popd_removes_from_top() {
let mut meta = MetaTab::new();
meta.push_dir(PathBuf::from("/etc"));
meta.push_dir(PathBuf::from("/var"));
meta.push_dir(PathBuf::from("/tmp"));
// popd (no args) or popd +0: pop from front
let popped = meta.pop_dir();
assert_eq!(popped, Some(PathBuf::from("/tmp")));
assert_eq!(meta.dirs().len(), 2);
}
#[test]
fn dirstack_popd_plus_n_offset() {
let mut meta = MetaTab::new();
// Stored: [/tmp, /var, /etc] (front to back)
meta.push_dir(PathBuf::from("/etc"));
meta.push_dir(PathBuf::from("/var"));
meta.push_dir(PathBuf::from("/tmp"));
// popd +2: full stack is [cwd, /tmp, /var, /etc]
// +2 = /var, which is stored index 1 (n-1 = 2-1 = 1)
let removed = meta.dirs_mut().remove(1); // n-1 for +N
assert_eq!(removed, Some(PathBuf::from("/var")));
let remaining: Vec<_> = meta.dirs().iter().collect();
assert_eq!(
remaining,
vec![&PathBuf::from("/tmp"), &PathBuf::from("/etc"),]
);
}
#[test]
fn dirstack_popd_minus_zero() {
let mut meta = MetaTab::new();
// Stored: [/tmp, /var, /etc]
meta.push_dir(PathBuf::from("/etc"));
meta.push_dir(PathBuf::from("/var"));
meta.push_dir(PathBuf::from("/tmp"));
// popd -0: remove bottom (back)
// actual = len - 1 - 0 = 2, via checked_sub(0+1) = checked_sub(1) = 2
let len = meta.dirs().len();
let actual = len.checked_sub(1).unwrap();
let removed = meta.dirs_mut().remove(actual);
assert_eq!(removed, Some(PathBuf::from("/etc")));
}
#[test]
fn dirstack_popd_minus_n() {
let mut meta = MetaTab::new();
// Stored: [/tmp, /var, /etc, /usr]
meta.push_dir(PathBuf::from("/usr"));
meta.push_dir(PathBuf::from("/etc"));
meta.push_dir(PathBuf::from("/var"));
meta.push_dir(PathBuf::from("/tmp"));
// popd -1: second from bottom = /etc
// actual = len - (1+1) = 4 - 2 = 2
let len = meta.dirs().len();
let actual = len.checked_sub(2).unwrap(); // n+1 = 2
let removed = meta.dirs_mut().remove(actual);
assert_eq!(removed, Some(PathBuf::from("/etc")));
}
#[test]
fn dirstack_clear() {
let mut meta = MetaTab::new();
meta.push_dir(PathBuf::from("/tmp"));
meta.push_dir(PathBuf::from("/var"));
meta.push_dir(PathBuf::from("/etc"));
meta.dirs_mut().clear();
assert!(meta.dirs().is_empty());
assert_eq!(meta.dir_stack_top(), None);
}

View File

@@ -1,43 +0,0 @@
use libsh::term::{Style, StyleSet, Styled};
use super::super::*;
#[test]
fn styled_simple() {
let input = "hello world";
let styled = input.styled(Style::Green);
insta::assert_snapshot!(styled)
}
#[test]
fn styled_multiple() {
let input = "styled text";
let styled = input.styled(Style::Red | Style::Bold | Style::Underline);
insta::assert_snapshot!(styled);
}
#[test]
fn styled_rgb() {
let input = "RGB styled text";
let styled = input.styled(Style::RGB(255, 99, 71)); // Tomato color
insta::assert_snapshot!(styled);
}
#[test]
fn styled_background() {
let input = "text with background";
let styled = input.styled(Style::BgBlue | Style::Bold);
insta::assert_snapshot!(styled);
}
#[test]
fn styled_set() {
let input = "multi-style text";
let style_set = StyleSet::new()
.add_style(Style::Magenta)
.add_style(Style::Italic);
let styled = input.styled(style_set);
insta::assert_snapshot!(styled);
}
#[test]
fn styled_reset() {
let input = "reset test";
let styled = input.styled(Style::Bold | Style::Reset);
insta::assert_snapshot!(styled);
}