added a bunch of tests
This commit is contained in:
642
src/tests/complete.rs
Normal file
642
src/tests/complete.rs
Normal 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());
|
||||
}
|
||||
Reference in New Issue
Block a user