opengrep 1.1.0

Advanced AST-aware code search tool with tree-sitter parsing and AI integration capabilities
Documentation
//! Integration tests for OpenGrep

use opengrep::{search::SearchEngine, Config};
use std::path::PathBuf;
use tempfile::TempDir;
use tokio;

#[tokio::test]
async fn test_basic_search() {
    let config = Config::default();
    let engine = SearchEngine::new(config);
    
    // Create test files
    let temp_dir = TempDir::new().unwrap();
    let test_file = temp_dir.path().join("test.rs");
    std::fs::write(&test_file, "fn main() {\n    println!(\"Hello\");\n}").unwrap();
    
    // Search
    let results = engine.search("main", &[temp_dir.path().to_path_buf()]).await.unwrap();
    
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].matches.len(), 1);
    assert_eq!(results[0].matches[0].line_number, 1);
}

#[tokio::test]
async fn test_ast_context() {
    let mut config = Config::default();
    config.output.show_ast_context = true;
    let engine = SearchEngine::new(config);
    
    // Create test file
    let temp_dir = TempDir::new().unwrap();
    let test_file = temp_dir.path().join("test.rs");
    std::fs::write(&test_file, r#"
struct Foo {
    bar: i32,
}

impl Foo {
    fn new() -> Self {
        Self { bar: 42 }
    }
}
"#).unwrap();
    
    // Search
    let results = engine.search("42", &[temp_dir.path().to_path_buf()]).await.unwrap();
    
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].matches.len(), 1);
    
    let ast_context = results[0].matches[0].ast_context.as_ref().unwrap();
    assert!(!ast_context.nodes.is_empty());
}

#[tokio::test]
async fn test_regex_search() {
    let mut config = Config::default();
    config.search.regex = true;
    let engine = SearchEngine::new(config);
    
    // Create test file
    let temp_dir = TempDir::new().unwrap();
    let test_file = temp_dir.path().join("test.rs");
    std::fs::write(&test_file, "fn test_one() {}\nfn test_two() {}").unwrap();
    
    // Search with regex
    let results = engine.search(r"test_\w+", &[temp_dir.path().to_path_buf()]).await.unwrap();
    
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].matches.len(), 2);
}

#[tokio::test]
async fn test_case_insensitive_search() {
    let mut config = Config::default();
    config.search.ignore_case = true;
    let engine = SearchEngine::new(config);
    
    // Create test file
    let temp_dir = TempDir::new().unwrap();
    let test_file = temp_dir.path().join("test.txt");
    std::fs::write(&test_file, "Hello World\nhello world\nHELLO WORLD").unwrap();
    
    // Search
    let results = engine.search("hello", &[temp_dir.path().to_path_buf()]).await.unwrap();
    
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].matches.len(), 3);
}

#[tokio::test]
async fn test_context_lines() {
    let mut config = Config::default();
    config.output.before_context = 1;
    config.output.after_context = 1;
    let engine = SearchEngine::new(config);
    
    // Create test file
    let temp_dir = TempDir::new().unwrap();
    let test_file = temp_dir.path().join("test.txt");
    std::fs::write(&test_file, "line 1\nline 2\nTARGET\nline 4\nline 5").unwrap();
    
    // Search
    let results = engine.search("TARGET", &[temp_dir.path().to_path_buf()]).await.unwrap();
    
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].matches.len(), 1);
    
    let match_item = &results[0].matches[0];
    assert_eq!(match_item.before_context.len(), 1);
    assert_eq!(match_item.after_context.len(), 1);
    assert_eq!(match_item.before_context[0], "line 2");
    assert_eq!(match_item.after_context[0], "line 4");
}

#[tokio::test]
async fn test_multiple_files() {
    let config = Config::default();
    let engine = SearchEngine::new(config);
    
    // Create test files
    let temp_dir = TempDir::new().unwrap();
    std::fs::write(temp_dir.path().join("file1.rs"), "fn main() {}").unwrap();
    std::fs::write(temp_dir.path().join("file2.rs"), "fn test() {}").unwrap();
    std::fs::write(temp_dir.path().join("file3.rs"), "struct Foo {}").unwrap();
    
    // Search for "fn"
    let results = engine.search("fn", &[temp_dir.path().to_path_buf()]).await.unwrap();
    
    assert_eq!(results.len(), 2); // file1.rs and file2.rs
    assert_eq!(results.iter().map(|r| r.matches.len()).sum::<usize>(), 2);
}

#[tokio::test]
async fn test_hidden_files() {
    let mut config = Config::default();
    config.search.hidden = true;
    let engine = SearchEngine::new(config);
    
    // Create hidden file
    let temp_dir = TempDir::new().unwrap();
    std::fs::write(temp_dir.path().join(".hidden"), "secret content").unwrap();
    
    // Search
    let results = engine.search("secret", &[temp_dir.path().to_path_buf()]).await.unwrap();
    
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].matches.len(), 1);
}

#[tokio::test]
async fn test_max_file_size() {
    let mut config = Config::default();
    config.search.max_file_size = Some(10); // 10 bytes
    let engine = SearchEngine::new(config);
    
    // Create large file
    let temp_dir = TempDir::new().unwrap();
    let large_content = "a".repeat(100);
    std::fs::write(temp_dir.path().join("large.txt"), &large_content).unwrap();
    
    // Search should skip large file
    let results = engine.search("a", &[temp_dir.path().to_path_buf()]).await.unwrap();
    
    assert_eq!(results.len(), 0);
}

#[test]
fn test_language_detection() {
    use opengrep::parsers::detect_language;
    
    assert_eq!(detect_language(&PathBuf::from("test.rs")), Some("rust".to_string()));
    assert_eq!(detect_language(&PathBuf::from("test.py")), Some("python".to_string()));
    assert_eq!(detect_language(&PathBuf::from("test.js")), Some("javascript".to_string()));
    assert_eq!(detect_language(&PathBuf::from("test.unknown")), None);
}

#[test]
fn test_config_from_cli() {
    use opengrep::cli::{Cli, OutputFormat};
    use clap::Parser;
    
    let cli = Cli::parse_from(&[
        "opengrep", 
        "-i", 
        "-n", 
        "-B", "3", 
        "-A", "3", 
        "pattern"
    ]);
    
    let config = opengrep::Config::from_cli(&cli).unwrap();
    
    assert!(config.search.ignore_case);
    assert!(config.output.line_numbers);
    assert_eq!(config.output.before_context, 3);
    assert_eq!(config.output.after_context, 3);
}

#[cfg(feature = "ai")]
#[tokio::test]
async fn test_ai_insights() {
    use opengrep::ai::AiService;
    use opengrep::AiConfig;
    
    // Skip if no API key
    if std::env::var("OPENAI_API_KEY").is_err() {
        return;
    }
    
    let config = AiConfig {
        api_key: std::env::var("OPENAI_API_KEY").unwrap(),
        model: "gpt-4o-mini".to_string(),
        enable_insights: true,
        enable_explanation: true,
        max_tokens: 100,
    };
    
    let ai_service = AiService::new(&config).unwrap();
    
    // Test pattern suggestion
    let patterns = ai_service.suggest_search_patterns("find memory leaks").await.unwrap();
    assert!(!patterns.is_empty());
    
    // Test code explanation
    let explanation = ai_service.explain_code("unsafe { ptr::read(ptr) }", Some("rust")).await.unwrap();
    assert!(!explanation.is_empty());
}