added a bunch of tests

This commit is contained in:
2026-02-18 23:54:25 -05:00
parent d77c2f39b8
commit 8354ad400d
14 changed files with 2595 additions and 53 deletions

642
src/tests/complete.rs Normal file
View File

@@ -0,0 +1,642 @@
use std::env;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
use crate::prompt::readline::complete::Completer;
use crate::state::{write_logic, write_vars, VarFlags};
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 (in_cmd, _) = completer.get_completion_context("ech", 3);
assert!(in_cmd, "Should be in command context at start");
// After whitespace - still command if no command yet
let (in_cmd, _) = completer.get_completion_context(" ech", 5);
assert!(in_cmd, "Should be in command context after whitespace");
}
#[test]
fn context_detection_argument_position() {
let completer = Completer::new();
// After a complete command - argument context
let (in_cmd, _) = completer.get_completion_context("echo hello", 10);
assert!(!in_cmd, "Should be in argument context after command");
let (in_cmd, _) = completer.get_completion_context("ls -la /tmp", 11);
assert!(!in_cmd, "Should be in argument context");
}
#[test]
fn context_detection_nested_command_sub() {
let completer = Completer::new();
// Inside $() - should be command context
let (in_cmd, _) = completer.get_completion_context("echo \"$(ech", 11);
assert!(in_cmd, "Should be in command context inside $()");
// After command in $() - argument context
let (in_cmd, _) = completer.get_completion_context("echo \"$(echo hell", 17);
assert!(!in_cmd, "Should be in argument context inside $()");
}
#[test]
fn context_detection_pipe() {
let completer = Completer::new();
// After pipe - command context
let (in_cmd, _) = completer.get_completion_context("ls | gre", 8);
assert!(in_cmd, "Should be in command context after pipe");
}
#[test]
fn context_detection_command_sep() {
let completer = Completer::new();
// After semicolon - command context
let (in_cmd, _) = completer.get_completion_context("echo foo; l", 11);
assert!(in_cmd, "Should be in command context after semicolon");
// After && - command context
let (in_cmd, _) = completer.get_completion_context("true && l", 9);
assert!(in_cmd, "Should be in command context after &&");
}
// ============================================================================
// 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());
}