test_utils/
lib.rs

1#![doc(
2    html_logo_url = "https://ajuvercr.github.io/semantic-web-lsp/assets/icons/favicon.png",
3    html_favicon_url = "https://ajuvercr.github.io/semantic-web-lsp/assets/icons/favicon.ico"
4)]
5use std::{
6    collections::HashMap,
7    fmt::Display,
8    fs::{exists, remove_dir_all},
9    future::Future,
10    path::PathBuf,
11    pin::Pin,
12    str::FromStr as _,
13    sync::{
14        atomic::{AtomicU32, Ordering},
15        Arc,
16    },
17    task::{Context, Poll},
18    time::Duration,
19};
20
21use async_std::fs::{self, read_to_string};
22use bevy_ecs::prelude::*;
23use futures::{
24    channel::mpsc::{unbounded, UnboundedReceiver},
25    lock::Mutex,
26    FutureExt as _,
27};
28use lsp_core::{
29    client::{Client, ClientSync, Resp},
30    components::*,
31    prelude::{
32        diagnostics::{DiagnosticItem, DiagnosticPublisher},
33        Fs, FsTrait,
34    },
35    setup_schedule_labels,
36    systems::{handle_tasks, spawn_or_insert},
37    Startup,
38};
39use lsp_types::{Diagnostic, MessageType, TextDocumentItem, Url};
40
41#[derive(Resource, Debug, Clone)]
42pub struct TestClient {
43    logs: Arc<Mutex<Vec<(MessageType, String)>>>,
44    diagnostics: Arc<Mutex<Vec<(Url, Vec<lsp_types::Diagnostic>)>>>,
45    locations: HashMap<String, String>,
46    tasks_running: Arc<std::sync::atomic::AtomicU32>,
47    executor: Arc<async_executor::Executor<'static>>,
48}
49
50impl TestClient {
51    pub fn new() -> Self {
52        // let pool = LocalPool::new();
53        Self {
54            logs: Default::default(),
55            diagnostics: Default::default(),
56            locations: Default::default(),
57            tasks_running: Arc::new(AtomicU32::new(0)),
58            executor: Arc::new(async_executor::Executor::new()),
59        }
60    }
61}
62impl Default for TestClient {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl TestClient {
69    pub fn add_res(&mut self, loc: &str, cont: &str) {
70        self.locations.insert(loc.to_string(), cont.to_string());
71    }
72}
73
74impl TestClient {
75    pub async fn await_futures<F: FnMut()>(&self, mut tick: F) {
76        tick();
77        while self.tasks_running.load(Ordering::Relaxed) != 0 {
78            self.executor.tick().await;
79            tick();
80        }
81    }
82}
83
84#[tower_lsp::async_trait]
85impl Client for TestClient {
86    async fn log_message<M: Display + Sync + Send + 'static>(&self, ty: MessageType, msg: M) -> () {
87        let mut lock = self.logs.lock().await;
88        lock.push((ty, msg.to_string()));
89    }
90
91    async fn publish_diagnostics(
92        &self,
93        uri: Url,
94        diags: Vec<Diagnostic>,
95        _version: Option<i32>,
96    ) -> () {
97        let mut lock = self.diagnostics.lock().await;
98        lock.push((uri, diags));
99    }
100}
101
102struct Sendable<T>(pub T);
103
104// Safety: WebAssembly will only ever run in a single-threaded context.
105unsafe impl<T> Send for Sendable<T> {}
106impl<O, T> Future for Sendable<T>
107where
108    T: Future<Output = O>,
109{
110    type Output = O;
111
112    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
113        // Safely access the inner future
114        let inner = unsafe { self.map_unchecked_mut(|s| &mut s.0) };
115        inner.poll(cx)
116    }
117}
118
119impl ClientSync for TestClient {
120    fn spawn<F: std::future::Future<Output = ()> + 'static>(&self, fut: F) {
121        self.tasks_running.fetch_add(1, Ordering::AcqRel);
122        let tr = self.tasks_running.clone();
123        let fut = async move {
124            fut.await;
125            tr.fetch_sub(1, Ordering::AcqRel);
126        };
127        self.executor.spawn(Sendable(fut)).detach();
128    }
129
130    fn fetch(
131        &self,
132        url: &str,
133        _headers: &std::collections::HashMap<String, String>,
134    ) -> std::pin::Pin<
135        Box<dyn Send + std::future::Future<Output = Result<lsp_core::client::Resp, String>>>,
136    > {
137        let body = self.locations.get(url).cloned();
138        Sendable(async move {
139            let mut headers = Vec::new();
140            async_std::task::sleep(Duration::from_millis(200)).await;
141            headers.push(("Content-Type".to_string(), "text/turtle".to_string()));
142            let status = body.is_some().then_some(200).unwrap_or(404);
143            Ok(Resp {
144                headers,
145                body: body.unwrap_or_default(),
146                status,
147            })
148        })
149        .boxed()
150    }
151}
152
153#[derive(Debug)]
154pub struct TestFs(PathBuf);
155impl TestFs {
156    pub fn new() -> Self {
157        let mut tmp_dir = std::env::temp_dir();
158        tmp_dir.push("swls");
159        tmp_dir.push("test");
160
161        if exists(&tmp_dir).unwrap_or(false) {
162            let _ = remove_dir_all(&tmp_dir);
163        }
164
165        Self(tmp_dir)
166    }
167    pub async fn empty(&self) {}
168}
169
170#[tower_lsp::async_trait]
171impl FsTrait for TestFs {
172    fn virtual_url(&self, url: &str) -> Option<lsp_types::Url> {
173        let mut pb = self.0.clone();
174        if let Ok(url) = lsp_types::Url::parse(url) {
175            pb.push(url.path());
176        } else {
177            pb.push(url);
178        }
179        lsp_types::Url::from_file_path(pb).ok()
180    }
181
182    async fn read_file(&self, url: &lsp_types::Url) -> Option<String> {
183        let fp = url.to_file_path().ok()?;
184        let content = read_to_string(fp).await.ok()?;
185        Some(content)
186    }
187
188    async fn write_file(&self, url: &lsp_types::Url, content: &str) -> Option<()> {
189        let fp = url.to_file_path().ok()?;
190        if let Some(parent) = fp.parent() {
191            fs::create_dir_all(parent).await.ok()?;
192        }
193        fs::write(fp, content.as_bytes()).await.ok()
194    }
195}
196
197pub fn setup_world(
198    client: TestClient,
199    f: impl FnOnce(&mut World) -> (),
200) -> (World, UnboundedReceiver<DiagnosticItem>) {
201    let mut world = World::new();
202    setup_schedule_labels::<TestClient>(&mut world);
203
204    let (tx, rx) = unbounded();
205    world.insert_resource(CommandSender(tx));
206    world.insert_resource(CommandReceiver(rx));
207    world.insert_resource(client);
208    world.insert_resource(Fs(Arc::new(TestFs::new())));
209
210    world.schedule_scope(lsp_core::Tasks, |_, schedule| {
211        schedule.add_systems(handle_tasks);
212    });
213
214    f(&mut world);
215
216    let (publisher, rx) = DiagnosticPublisher::new();
217    world.insert_resource(publisher);
218    world.insert_resource(ServerConfig::default());
219
220    world.run_schedule(Startup);
221
222    (world, rx)
223}
224
225pub fn create_file(
226    world: &mut World,
227    content: &str,
228    url: &str,
229    lang: &str,
230    bundle: impl Bundle,
231) -> Entity {
232    let url = Url::from_str(url).unwrap();
233    let item = TextDocumentItem {
234        version: 1,
235        uri: url.clone(),
236        language_id: String::from(lang),
237        text: String::new(),
238    };
239
240    spawn_or_insert(
241        url.clone(),
242        (
243            Source(content.to_string()),
244            RopeC(ropey::Rope::from_str(content)),
245            Label(url), // this might crash
246            Wrapped(item),
247            Types(HashMap::new()),
248        ),
249        Some(lang.into()),
250        bundle,
251    )(world)
252}
253
254pub fn debug_world(world: &mut World) {
255    for e in world.query::<Entity>().iter(&world) {
256        let e = world.entity(e);
257        if let Some(l) = e.get::<Label>() {
258            println!("-- Entity {} -- ", l.as_str());
259        } else {
260            println!("-- Nameless entity --");
261        }
262        for c in world.components().iter() {
263            if e.contains_id(c.id()) {
264                println!("c {}", c.name(),);
265            }
266        }
267    }
268}