rust-analyzer-cli 0.4.1

A library and CLI tool built on top of rust-analyzer for codebase navigation
Documentation
use reqwest::Client;
use rust_analyzer_cli::daemon::server::start_daemon_server;
use rust_analyzer_cli::lsp::types::*;
use std::net::TcpListener;
use std::path::PathBuf;
use tokio::time::{Duration, sleep};

const FIXTURE_FILE: &str = "tests/code_example.rs";
const FIXTURE_DEFINITION_LINE: u32 = 151;
const FIXTURE_DEFINITION_COL: u32 = 5;

fn get_free_port() -> u16 {
    let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind free port");
    let port = listener.local_addr().unwrap().port();
    drop(listener);
    port
}

#[tokio::test]
async fn test_daemon_full_workflow() {
    let port = get_free_port();
    let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"));

    // Spawn daemon server in background
    let daemon_workspace = workspace.clone();
    tokio::spawn(async move {
        if let Err(e) = start_daemon_server(daemon_workspace, port).await {
            eprintln!("Daemon server error: {}", e);
        }
    });

    // Wait briefly for server to bind
    sleep(Duration::from_millis(500)).await;

    let client = Client::new();
    let base_url = format!("http://127.0.0.1:{}", port);

    // 1. Test GET /status
    let status_res = client
        .get(format!("{}/status", base_url))
        .send()
        .await
        .expect("Failed to send /status request");
    assert!(status_res.status().is_success());
    let status: DaemonStatusResponse = status_res.json().await.unwrap();
    let expected_root = workspace
        .canonicalize()
        .unwrap()
        .to_string_lossy()
        .to_string();
    assert_eq!(
        status.workspace_root.trim_start_matches(r"\\?\"),
        expected_root.trim_start_matches(r"\\?\")
    );

    // 2. Test POST /api/symbol with retry until rust-analyzer indexing completes
    let mut symbol_found = false;
    for _ in 0..20 {
        let symbol_req = SymbolQueryRequest {
            name: "ExampleStruct".to_string(),
            kind: "any".to_string(),
            exact: true,
        };
        let symbol_res = client
            .post(format!("{}/api/symbol", base_url))
            .json(&symbol_req)
            .send()
            .await
            .expect("Failed to send /api/symbol request");

        if symbol_res.status().is_success() {
            let symbols: Vec<SymbolItem> = symbol_res.json().await.unwrap();
            if symbols.iter().any(|s| s.name == "ExampleStruct") {
                symbol_found = true;
                break;
            }
        }
        sleep(Duration::from_millis(500)).await;
    }
    assert!(
        symbol_found,
        "Expected at least one symbol for ExampleStruct"
    );

    // 3. Test POST /api/outline
    let outline_req = OutlineQueryRequest {
        file: PathBuf::from(FIXTURE_FILE),
    };
    let outline_res = client
        .post(format!("{}/api/outline", base_url))
        .json(&outline_req)
        .send()
        .await
        .expect("Failed to send /api/outline request");
    assert!(outline_res.status().is_success());
    let outline: Vec<OutlineItem> = outline_res.json().await.unwrap();
    assert!(
        !outline.is_empty(),
        "Expected outline items for code_example"
    );
    assert!(outline.iter().any(|item| item.name == "ExampleStruct"));
    assert!(outline.iter().any(|item| item.name == "long_example"));

    // 4. Test POST /api/definition
    let def_req = DefinitionQueryRequest {
        file: PathBuf::from(FIXTURE_FILE),
        line: FIXTURE_DEFINITION_LINE,
        col: FIXTURE_DEFINITION_COL,
    };
    let def_res = client
        .post(format!("{}/api/definition", base_url))
        .json(&def_req)
        .send()
        .await
        .expect("Failed to send /api/definition request");
    assert!(def_res.status().is_success());
    let definitions: Vec<DefinitionItem> = def_res.json().await.unwrap();
    assert!(
        !definitions.is_empty(),
        "Expected definition for long_example"
    );
    assert!(
        definitions
            .iter()
            .any(|item| item.line < FIXTURE_DEFINITION_LINE)
    );

    // 5. Test POST /api/body
    let body_req = BodyQueryRequest {
        file: PathBuf::from(FIXTURE_FILE),
        line: FIXTURE_DEFINITION_LINE,
        col: FIXTURE_DEFINITION_COL,
        max_lines: 2,
    };

    let body_res = client
        .post(format!("{}/api/body", base_url))
        .json(&body_req)
        .send()
        .await
        .expect("Failed to send /api/body request");
    assert!(body_res.status().is_success());
    let body_item: BodyItem = body_res.json().await.unwrap();
    assert!(
        !body_item.body.is_empty(),
        "Expected non-empty body for long_example"
    );
    assert_eq!(body_item.body.lines().count(), 2);
    assert!(body_item.total_lines > 100);
    assert!(body_item.is_truncated);

    let default_body_res = client
        .post(format!("{}/api/body", base_url))
        .json(&serde_json::json!({
            "file": FIXTURE_FILE,
            "line": FIXTURE_DEFINITION_LINE,
            "col": FIXTURE_DEFINITION_COL,
        }))
        .send()
        .await
        .expect("Failed to send default /api/body request");
    assert!(default_body_res.status().is_success());
    let default_body: BodyItem = default_body_res.json().await.unwrap();
    assert_eq!(default_body.body.lines().count(), 100);
    assert!(default_body.is_truncated);

    // 6. Test POST /api/check
    let check_req = CheckQueryRequest { target: None };
    let check_res = client
        .post(format!("{}/api/check", base_url))
        .json(&check_req)
        .send()
        .await
        .expect("Failed to send /api/check request");
    assert!(check_res.status().is_success());
    let check: CheckResponse = check_res.json().await.unwrap();
    assert!(check.success);
}