1#![doc = include_str!("docs/testing.md")]
2
3use std::future::{Future, poll_fn};
4use std::path::{Path, PathBuf};
5use std::pin::pin;
6use std::process::Command;
7use std::sync::{Arc, mpsc};
8use std::thread::JoinHandle;
9use std::time::Duration;
10use tokio::sync::{oneshot, watch};
11
12use crate::LanguageId;
13use crate::daemon::{LspDaemon, Timeouts};
14use crate::error::DaemonError;
15use crate::socket_path::socket_path;
16use crate::uri::path_to_uri;
17pub use utils::testing::TestWorkspace;
18
19pub struct TestDaemon {
21 shutdown_tx: Option<oneshot::Sender<()>>,
22 task: Option<JoinHandle<Result<(), DaemonError>>>,
23 clock: Option<tokio::runtime::Handle>,
24}
25
26pub struct TestDaemonBuilder {
28 workspace_root: PathBuf,
29 language: LanguageId,
30 timeouts: Timeouts,
31 hooks: TestHooks,
32 paused_time: bool,
33}
34
35impl TestDaemon {
36 pub fn spawn(workspace_root: &Path, language: LanguageId) -> Result<Self, TestDaemonError> {
37 Self::builder(workspace_root, language).spawn()
38 }
39
40 pub fn builder(workspace_root: &Path, language: LanguageId) -> TestDaemonBuilder {
41 TestDaemonBuilder {
42 workspace_root: workspace_root.to_path_buf(),
43 language,
44 timeouts: Timeouts::default(),
45 hooks: TestHooks::default(),
46 paused_time: false,
47 }
48 }
49
50 pub async fn advance_time(&self, duration: Duration) {
51 self.clock
52 .as_ref()
53 .expect("enable paused_time on the daemon builder")
54 .spawn(async move { tokio::time::advance(duration).await })
55 .await
56 .expect("daemon clock task should complete");
57 }
58
59 pub fn shutdown(mut self) -> Result<(), TestDaemonError> {
60 if let Some(shutdown_tx) = self.shutdown_tx.take() {
61 let _ = shutdown_tx.send(());
62 }
63 self.finish()
64 }
65
66 fn finish(&mut self) -> Result<(), TestDaemonError> {
67 let task = self.task.take().expect("test daemon task should be present");
68 task.join().map_err(|_| TestDaemonError::ThreadPanicked)??;
69 Ok(())
70 }
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum TestPoint {
75 ReadyWait,
76 RequestReceived(String),
77 DocumentOpen(lsp_types::Uri),
78 DocumentSyncWait(lsp_types::Uri),
79 DiagnosticsWait(lsp_types::Uri),
80}
81
82#[derive(Clone, Default)]
83pub struct TestGate {
84 state: Arc<watch::Sender<(bool, bool)>>,
85}
86
87impl TestGate {
88 pub async fn reached(&self) {
89 let _ = self.state.subscribe().wait_for(|(reached, _)| *reached).await;
90 }
91
92 pub fn release(&self) {
93 self.state.send_modify(|(_, released)| *released = true);
94 }
95
96 fn reach(&self) {
97 self.state.send_modify(|(reached, _)| *reached = true);
98 }
99
100 async fn pause(&self) {
101 self.reach();
102 let _ = self.state.subscribe().wait_for(|(_, released)| *released).await;
103 }
104}
105
106impl TestDaemonBuilder {
107 pub fn gate(mut self, point: TestPoint, gate: &TestGate) -> Self {
108 self.hooks.0.push((point, gate.clone()));
109 self
110 }
111
112 pub fn paused_time(mut self) -> Self {
113 self.paused_time = true;
114 self
115 }
116
117 pub fn request_timeout(mut self, timeout: Duration) -> Self {
118 self.timeouts.request = timeout;
119 self
120 }
121
122 pub fn diagnostics_timeout(mut self, timeout: Duration) -> Self {
123 self.timeouts.diagnostics = timeout;
124 self
125 }
126
127 pub fn diagnostics_settle(mut self, settle: Duration) -> Self {
128 self.timeouts.diagnostics_settle = settle;
129 self
130 }
131
132 pub fn spawn(self) -> Result<TestDaemon, TestDaemonError> {
133 let socket_path = socket_path(&self.workspace_root, self.language);
134 let daemon = LspDaemon::bind(socket_path, None, self.timeouts)?.with_test_hooks(self.hooks);
135 let runtime = if self.paused_time {
136 tokio::runtime::Builder::new_current_thread().enable_all().start_paused(true).build()
137 } else {
138 tokio::runtime::Builder::new_multi_thread().enable_all().build()
139 }
140 .map_err(TestDaemonError::Runtime)?;
141 let clock = self.paused_time.then(|| runtime.handle().clone());
142 let (shutdown_tx, shutdown_rx) = oneshot::channel();
143 let task = std::thread::spawn(move || {
144 let (release_tx, release_rx) = mpsc::channel::<()>();
146 let hold_clock = self.paused_time.then(|| runtime.spawn_blocking(move || release_rx.recv()));
147 let result = runtime.block_on(daemon.run_until_shutdown(shutdown_rx));
148 drop(release_tx);
149 if let Some(hold_clock) = hold_clock {
150 runtime.block_on(hold_clock).expect("clock guard should finish").ok();
151 }
152 result
153 });
154 Ok(TestDaemon { shutdown_tx: Some(shutdown_tx), task: Some(task), clock })
155 }
156}
157
158impl Drop for TestDaemon {
159 fn drop(&mut self) {
160 if let Some(shutdown_tx) = self.shutdown_tx.take() {
161 let _ = shutdown_tx.send(());
162 }
163 if let Some(task) = self.task.take() {
164 let _ = task.join();
165 }
166 }
167}
168
169#[derive(Debug, thiserror::Error)]
170pub enum TestDaemonError {
171 #[error("Daemon failed: {0}")]
172 Daemon(#[from] DaemonError),
173 #[error("Failed to create daemon runtime: {0}")]
174 Runtime(std::io::Error),
175 #[error("Daemon thread panicked")]
176 ThreadPanicked,
177}
178
179pub unsafe fn configure_fake_server(language: LanguageId, extra_args: &[&str]) {
186 let env_key = language.server().expect("language should have a configured server").env_key;
187 let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/common/fake_lsp_server.py");
188 let mut args = vec![script.to_string_lossy().into_owned()];
189 args.extend(extra_args.iter().map(ToString::to_string));
190
191 unsafe {
192 std::env::set_var(format!("AETHER_LSPD_SERVER_COMMAND_{env_key}"), "python3");
193 std::env::set_var(
194 format!("AETHER_LSPD_SERVER_ARGS_{env_key}"),
195 serde_json::to_string(&args).expect("fake server arguments should serialize"),
196 );
197 }
198}
199
200pub fn cargo_project(name: &str) -> TestWorkspace {
201 let manifest = format!(
202 r#"[package]
203name = "{name}"
204version = "0.1.0"
205edition = "2021"
206"#
207 );
208 let main = r#"fn main() {
209 println!("Hello, world!");
210}
211"#;
212 TestWorkspace::new().file("Cargo.toml", manifest).file("src/main.rs", main)
213}
214
215pub fn node_project(name: &str) -> TestWorkspace {
216 let package = format!(
217 r#"{{
218 "name": "{name}",
219 "version": "0.1.0"
220}}"#
221 );
222 let tsconfig = r#"{
223 "compilerOptions": {
224 "strict": true,
225 "noEmit": true
226 }
227}"#;
228 let project =
229 TestWorkspace::new().file("package.json", package).file("tsconfig.json", tsconfig).file("src/index.ts", "");
230
231 let args = ["install", "--save-dev", "--no-audit", "--no-fund", "--prefer-offline", TYPESCRIPT_PACKAGE];
232 let output = Command::new("npm").args(args).current_dir(project.root()).output().expect("failed to run npm");
233 assert!(output.status.success(), "npm {} failed:\n{}", args.join(" "), String::from_utf8_lossy(&output.stderr));
234 project
235}
236
237pub fn file_uri(path: &Path) -> lsp_types::Uri {
238 path_to_uri(path).expect("Invalid file path")
239}
240
241#[derive(Clone, Default)]
242pub(crate) struct TestHooks(Vec<(TestPoint, TestGate)>);
243
244impl TestHooks {
245 pub(crate) async fn pause(&self, point: TestPoint) {
246 if let Some(gate) = self.gate(&point) {
247 gate.pause().await;
248 }
249 }
250
251 pub(crate) async fn observe_pending<T>(&self, point: TestPoint, future: impl Future<Output = T>) -> T {
252 let mut future = pin!(future);
253 poll_fn(|cx| {
254 let result = future.as_mut().poll(cx);
255 if result.is_pending()
256 && let Some(gate) = self.gate(&point)
257 {
258 gate.reach();
259 }
260 result
261 })
262 .await
263 }
264
265 fn gate(&self, point: &TestPoint) -> Option<&TestGate> {
266 self.0.iter().find_map(|(at, gate)| (at == point).then_some(gate))
267 }
268}
269
270const TYPESCRIPT_PACKAGE: &str = "typescript@7.0.2";