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 = 156;
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
}
fn fixture_position(line_fragment: &str, target: &str) -> (u32, u32) {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(FIXTURE_FILE);
let content = std::fs::read_to_string(path).expect("fixture should be readable");
for (index, line) in content.lines().enumerate() {
if line.contains(line_fragment)
&& let Some(byte_offset) = line.find(target)
{
let utf16_col = line[..byte_offset].encode_utf16().count() as u32 + 1;
return (index as u32 + 1, utf16_col);
}
}
panic!("fixture does not contain target '{target}' on line matching '{line_fragment}'");
}
#[tokio::test]
async fn test_daemon_full_workflow() {
let port = get_free_port();
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
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);
}
});
sleep(Duration::from_millis(500)).await;
let client = Client::new();
let base_url = format!("http://127.0.0.1:{}", port);
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"\\?\")
);
assert_eq!(status.state, DaemonState::Starting);
let mut symbol_found = false;
for _ in 0..20 {
let symbol_req = SymbolQueryRequest {
name: "ExampleStruct".to_string(),
kind: "any".to_string(),
exact: true,
include_body: true,
max_lines: 10,
};
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" && s.body.is_some())
{
symbol_found = true;
break;
}
}
sleep(Duration::from_millis(500)).await;
}
assert!(
symbol_found,
"Expected at least one symbol for ExampleStruct"
);
let outline_req = OutlineQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
include_body: true,
max_lines: 10,
};
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"));
assert!(
outline
.iter()
.find(|item| item.name == "ExampleStruct")
.and_then(|item| item.body.as_ref())
.is_some()
);
let def_req = DefinitionQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: FIXTURE_DEFINITION_LINE,
col: FIXTURE_DEFINITION_COL,
include_body: true,
max_lines: 2,
};
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)
);
assert!(definitions.iter().any(|item| item.body.is_some()));
let relation_res = client
.post(format!("{}/api/relations", base_url))
.json(&RelationQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: 20,
col: 12,
mode: RelationMode::Implementations,
})
.send()
.await
.expect("Failed to send /api/relations request");
assert!(relation_res.status().is_success());
let relations: Vec<RelationItem> = relation_res.json().await.unwrap();
assert!(relations.iter().any(|item| item.line == 24));
let (unicode_line, unicode_col) = fixture_position(
"let _ = \"🦀\".len() + unicode_target() as usize;",
"unicode_target()",
);
let unicode_definition_res = client
.post(format!("{}/api/definition", base_url))
.json(&DefinitionQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: unicode_line,
col: unicode_col,
include_body: false,
max_lines: 100,
})
.send()
.await
.expect("Failed to send Unicode definition request");
assert!(unicode_definition_res.status().is_success());
let unicode_definitions: Vec<DefinitionItem> = unicode_definition_res.json().await.unwrap();
assert!(
unicode_definitions
.iter()
.any(|item| item.line < unicode_line)
);
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);
let hover_req = HoverQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: FIXTURE_DEFINITION_LINE,
col: FIXTURE_DEFINITION_COL,
};
let hover_res = client
.post(format!("{}/api/hover", base_url))
.json(&hover_req)
.send()
.await
.expect("Failed to send /api/hover request");
assert!(hover_res.status().is_success());
let hover: Option<HoverItem> = hover_res.json().await.unwrap();
let hover = hover.expect("Expected hover documentation for long_example");
assert!(
hover
.contents
.contains("Runs a long example for Hover tests.")
);
assert!(hover.contents.contains("```rust"));
assert!(hover.range.is_some());
let empty_hover_res = client
.post(format!("{}/api/hover", base_url))
.json(&HoverQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: 4,
col: 1,
})
.send()
.await
.expect("Failed to send empty /api/hover request");
assert!(empty_hover_res.status().is_success());
let empty_hover: Option<HoverItem> = empty_hover_res.json().await.unwrap();
assert!(empty_hover.is_none());
let references_res = client
.post(format!("{}/api/references", base_url))
.json(&ReferenceQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: 156,
col: 5,
})
.send()
.await
.expect("Failed to send references request");
assert!(references_res.status().is_success());
let references: Vec<ReferenceItem> = references_res.json().await.unwrap();
assert!(!references.is_empty());
assert!(references.iter().all(|item| item.end_col > item.col));
let empty_references_res = client
.post(format!("{}/api/references", base_url))
.json(&ReferenceQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: 4,
col: 1,
})
.send()
.await
.expect("Failed to send empty references request");
assert!(empty_references_res.status().is_success());
let empty_references: Vec<ReferenceItem> = empty_references_res.json().await.unwrap();
assert!(empty_references.is_empty());
let calls_depth_one = client
.post(format!("{}/api/calls", base_url))
.json(&CallQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: 167,
col: 8,
direction: CallDirection::Outgoing,
depth: 1,
})
.send()
.await
.expect("Failed to send depth-one calls request");
assert!(calls_depth_one.status().is_success());
let calls_depth_one: Vec<CallItem> = calls_depth_one.json().await.unwrap();
assert!(
calls_depth_one
.iter()
.any(|item| item.name == "depth_middle")
);
assert!(!calls_depth_one.iter().any(|item| item.name == "depth_leaf"));
assert!(
calls_depth_one
.iter()
.all(|item| item.direction == CallDirection::Outgoing && item.depth == 1)
);
let calls_depth_two = client
.post(format!("{}/api/calls", base_url))
.json(&CallQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: 167,
col: 8,
direction: CallDirection::Outgoing,
depth: 2,
})
.send()
.await
.expect("Failed to send depth-two calls request");
assert!(calls_depth_two.status().is_success());
let calls_depth_two: Vec<CallItem> = calls_depth_two.json().await.unwrap();
let mut call_keys = std::collections::HashSet::new();
for item in &calls_depth_two {
assert!(call_keys.insert((item.file.clone(), item.line, item.col)));
assert!(item.end_col > item.col);
}
assert!(calls_depth_two.iter().any(|item| item.name == "depth_leaf"));
assert!(calls_depth_two.len() >= calls_depth_one.len());
let (long_example_line, long_example_col) =
fixture_position("pub fn long_example", "long_example");
let incoming_calls_res = client
.post(format!("{}/api/calls", base_url))
.json(&CallQueryRequest {
file: PathBuf::from(FIXTURE_FILE),
line: long_example_line,
col: long_example_col,
direction: CallDirection::Incoming,
depth: 1,
})
.send()
.await
.expect("Failed to send incoming calls request");
assert!(incoming_calls_res.status().is_success());
let incoming_calls: Vec<CallItem> = incoming_calls_res.json().await.unwrap();
assert!(
incoming_calls
.iter()
.any(|item| item.name == "use_long_example")
);
assert!(
incoming_calls
.iter()
.all(|item| item.direction == CallDirection::Incoming && item.depth == 1)
);
let removed_cursor_res = client
.post(format!("{}/api/cursor", base_url))
.json(&serde_json::json!({
"file": FIXTURE_FILE,
"line": 167,
"col": 8,
"mode": "outgoing",
"depth": 1
}))
.send()
.await
.expect("Failed to probe removed cursor endpoint");
assert_eq!(removed_cursor_res.status(), reqwest::StatusCode::NOT_FOUND);
let invalid_calls_res = client
.post(format!("{}/api/calls", base_url))
.json(&serde_json::json!({
"file": FIXTURE_FILE,
"line": 167,
"col": 8,
"direction": "invalid",
"depth": 1
}))
.send()
.await
.expect("Failed to send invalid calls request");
assert_eq!(invalid_calls_res.status(), reqwest::StatusCode::BAD_REQUEST);
let invalid_relations_res = client
.post(format!("{}/api/relations", base_url))
.json(&serde_json::json!({
"file": FIXTURE_FILE,
"line": 20,
"col": 12,
"mode": "invalid"
}))
.send()
.await
.expect("Failed to send invalid relations request");
assert_eq!(
invalid_relations_res.status(),
reqwest::StatusCode::BAD_REQUEST
);
let removed_check_res = client
.post(format!("{}/api/check", base_url))
.send()
.await
.expect("Failed to probe removed /api/check endpoint");
assert_eq!(removed_check_res.status(), reqwest::StatusCode::NOT_FOUND);
let refresh_res = client
.post(format!("{}/refresh", base_url))
.send()
.await
.expect("Failed to send /refresh request");
assert!(refresh_res.status().is_success());
let refresh_payload: serde_json::Value = refresh_res.json().await.unwrap();
assert_eq!(refresh_payload["success"], true);
let ready_status_res = client
.get(format!("{}/status", base_url))
.send()
.await
.expect("Failed to re-read /status");
let ready_status: DaemonStatusResponse = ready_status_res.json().await.unwrap();
assert_eq!(ready_status.state, DaemonState::Ready);
}