From 8c91748a7e7313d275c8f5f8dd29a28cf8a8d32b Mon Sep 17 00:00:00 2001 From: pagedmov Date: Thu, 5 Mar 2026 13:46:55 -0500 Subject: [PATCH] Remove test module and delete all test files --- src/main.rs | 2 - src/tests/complete.rs | 767 ----------------------------------- src/tests/error.rs | 174 -------- src/tests/expand.rs | 395 ------------------ src/tests/getopt.rs | 51 --- src/tests/highlight.rs | 668 ------------------------------ src/tests/lexer.rs | 52 --- src/tests/mod.rs | 45 --- src/tests/parser.rs | 234 ----------- src/tests/readline.rs | 700 -------------------------------- src/tests/redir.rs | 428 -------------------- src/tests/script.rs | 51 --- src/tests/state.rs | 895 ----------------------------------------- src/tests/term.rs | 43 -- 14 files changed, 4505 deletions(-) delete mode 100644 src/tests/complete.rs delete mode 100644 src/tests/error.rs delete mode 100644 src/tests/expand.rs delete mode 100644 src/tests/getopt.rs delete mode 100644 src/tests/highlight.rs delete mode 100644 src/tests/lexer.rs delete mode 100644 src/tests/mod.rs delete mode 100644 src/tests/parser.rs delete mode 100644 src/tests/readline.rs delete mode 100644 src/tests/redir.rs delete mode 100644 src/tests/script.rs delete mode 100644 src/tests/state.rs delete mode 100644 src/tests/term.rs diff --git a/src/main.rs b/src/main.rs index 0cc32fa..24b0e74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,8 +16,6 @@ pub mod readline; pub mod shopt; pub mod signal; pub mod state; -#[cfg(test)] -pub mod tests; use std::os::fd::BorrowedFd; use std::process::ExitCode; diff --git a/src/tests/complete.rs b/src/tests/complete.rs deleted file mode 100644 index 965c77e..0000000 --- a/src/tests/complete.rs +++ /dev/null @@ -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()); -} diff --git a/src/tests/error.rs b/src/tests/error.rs deleted file mode 100644 index 5ae5eaa..0000000 --- a/src/tests/error.rs +++ /dev/null @@ -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::>>(); - - 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::>(); - - 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::>(); - - 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::>(); - - 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::>(); - - 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::>(); - - 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::>(); - - 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) -} diff --git a/src/tests/expand.rs b/src/tests/expand.rs deleted file mode 100644 index 1dbbe9e..0000000 --- a/src/tests/expand.rs +++ /dev/null @@ -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 = 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"); -} diff --git a/src/tests/getopt.rs b/src/tests/getopt.rs deleted file mode 100644 index a40bf26..0000000 --- a/src/tests/getopt.rs +++ /dev/null @@ -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::>(); - - 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::>(); - - let (words, opts) = get_opts(raw); - insta::assert_debug_snapshot!(words); - insta::assert_debug_snapshot!(opts); -} diff --git a/src/tests/highlight.rs b/src/tests/highlight.rs deleted file mode 100644 index a809cf0..0000000 --- a/src/tests/highlight.rs +++ /dev/null @@ -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 { - 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) -} diff --git a/src/tests/lexer.rs b/src/tests/lexer.rs deleted file mode 100644 index dd5422c..0000000 --- a/src/tests/lexer.rs +++ /dev/null @@ -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) -} diff --git a/src/tests/mod.rs b/src/tests/mod.rs deleted file mode 100644 index 59408e2..0000000 --- a/src/tests/mod.rs +++ /dev/null @@ -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(input: &str, filter: F1) -> Vec -where - F1: Fn(&Node) -> bool, -{ - let mut nodes = vec![]; - let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) - .map(|tk| tk.unwrap()) - .collect::>(); - let mut parsed_nodes = ParseStream::new(tokens) - .map(|nd| nd.unwrap()) - .collect::>(); - - for node in parsed_nodes.iter_mut() { - node_operation(node, &filter, &mut |node: &mut Node| { - nodes.push(node.clone()) - }); - } - nodes -} diff --git a/src/tests/parser.rs b/src/tests/parser.rs deleted file mode 100644 index 28a6ba6..0000000 --- a/src/tests/parser.rs +++ /dev/null @@ -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 = 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) -} diff --git a/src/tests/readline.rs b/src/tests/readline.rs deleted file mode 100644 index 8ac8f1d..0000000 --- a/src/tests/readline.rs +++ /dev/null @@ -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, -} - -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 { - 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, 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, 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́")); // 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 \\<\\>delimited> text", - 0 - ), - ("this buffer has <> text".into(), 17) - ); - assert_eq!( - normal_cmd( - "da>", - "this buffer has \\<\\>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 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 from position 0 finding a char that only appears later - assert_eq!( - normal_cmd("fo", "hello world", 0), - ("hello world".into(), 4) - ); - // f 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." - ) -} -*/ diff --git a/src/tests/redir.rs b/src/tests/redir.rs deleted file mode 100644 index a016296..0000000 --- a/src/tests/redir.rs +++ /dev/null @@ -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::>(); - - let mut nodes = ParseStream::new(tokens).flatten().collect::>(); - - 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); -} diff --git a/src/tests/script.rs b/src/tests/script.rs deleted file mode 100644 index 6ef63a9..0000000 --- a/src/tests/script.rs +++ /dev/null @@ -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") -} diff --git a/src/tests/state.rs b/src/tests/state.rs deleted file mode 100644 index 61d1ed9..0000000 --- a/src/tests/state.rs +++ /dev/null @@ -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::().unwrap(), - ShellParam::Status - )); - assert!(matches!( - "$".parse::().unwrap(), - ShellParam::ShPid - )); - assert!(matches!( - "!".parse::().unwrap(), - ShellParam::LastJob - )); - assert!(matches!( - "0".parse::().unwrap(), - ShellParam::ShellName - )); - assert!(matches!( - "@".parse::().unwrap(), - ShellParam::AllArgs - )); - assert!(matches!( - "*".parse::().unwrap(), - ShellParam::AllArgsStr - )); - assert!(matches!( - "#".parse::().unwrap(), - ShellParam::ArgCount - )); - - match "1".parse::().unwrap() { - ShellParam::Pos(n) => assert_eq!(n, 1), - _ => panic!("Expected Pos(1)"), - } - - match "42".parse::().unwrap() { - ShellParam::Pos(n) => assert_eq!(n, 42), - _ => panic!("Expected Pos(42)"), - } - - assert!("invalid".parse::().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); -} diff --git a/src/tests/term.rs b/src/tests/term.rs deleted file mode 100644 index 43c4141..0000000 --- a/src/tests/term.rs +++ /dev/null @@ -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); -}