Skip to content

Commit 10867ff

Browse files
committed
Add new goto definition tests for workspace package import() and importFrom()
With some extracted support infrastructure: `DescriptionWriter`, `NamespaceWriter`, and the generic `TestSourceHandler`
1 parent 42e42f0 commit 10867ff

9 files changed

Lines changed: 375 additions & 107 deletions

File tree

crates/ark/src/lsp/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod find_references;
44
mod goto_definition;
55
mod main_loop;
66
mod rename;
7+
mod source_handler;
78
mod sources;
89
mod state;
910
mod state_handlers;

crates/ark/src/lsp/tests/goto_definition.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1+
use std::collections::HashMap;
2+
use std::sync::Arc;
3+
14
use aether_path::FilePath;
25
use assert_matches::assert_matches;
6+
use oak_db::Db;
7+
use oak_db::OakDatabase;
8+
use oak_scan::DbScan;
9+
use oak_semantic::library::Library;
310
use tower_lsp::lsp_types;
411
use tower_lsp::lsp_types::GotoDefinitionParams;
512
use tower_lsp::lsp_types::GotoDefinitionResponse;
613
use url::Url;
714

15+
use super::source_handler::TestBehavior;
16+
use super::source_handler::TestSourceHandler;
17+
use super::utils::did_change_workspace_folders;
818
use super::utils::insert_file;
919
use super::utils::make_state;
1020
use super::utils::range;
21+
use super::utils::test_client;
22+
use super::utils::write_sources;
23+
use super::utils::DescriptionWriter;
24+
use super::utils::NamespaceWriter;
1125
use crate::lsp::goto_definition::goto_definition;
26+
use crate::lsp::main_loop::init_aux_for_test;
27+
use crate::lsp::main_loop::GlobalState;
28+
use crate::lsp::main_loop::LspState;
29+
use crate::lsp::sources::SourceScheduler;
1230
use crate::lsp::state::WorldState;
1331
use crate::lsp::util::test_path;
1432

@@ -214,3 +232,135 @@ fn test_sourced_file_with_sequential_redef_offers_runtime_winner() {
214232
}
215233
);
216234
}
235+
236+
/// Goto-def from a bare `foo()` through to `foopkg::foo()`'s definition via the
237+
/// workspace package's `import(foopkg)`
238+
#[tokio::test]
239+
async fn test_goto_definition_resolves_unqualified_import_into_package() {
240+
let _aux = init_aux_for_test();
241+
242+
let handler = Arc::new(TestSourceHandler::new(HashMap::from([(
243+
String::from("foopkg"),
244+
TestBehavior::Success(vec![("foo.R", "foo <- function() 1\n")]),
245+
)])));
246+
247+
// Set up the library that our "installed" package lives in
248+
let library = tempfile::tempdir().unwrap();
249+
DescriptionWriter::new()
250+
.package("foopkg")
251+
.version("0.0.0")
252+
.built("dummy")
253+
.write(&library.path().join("foopkg"));
254+
NamespaceWriter::new()
255+
.export("foo")
256+
.write(&library.path().join("foopkg"));
257+
258+
let mut db = OakDatabase::new();
259+
db.set_library_paths(&[library.path().to_path_buf()]);
260+
261+
let mut state = GlobalState::from_parts(
262+
test_client(),
263+
WorldState::new(db, Library::new(vec![])),
264+
LspState::new(
265+
tokio::sync::mpsc::unbounded_channel().0,
266+
SourceScheduler::new(Some(handler)),
267+
),
268+
);
269+
270+
// The workspace folder is itself the package that imports `foo`.
271+
let workspace = tempfile::tempdir().unwrap();
272+
DescriptionWriter::new()
273+
.package("mypackage")
274+
.version("0.0.0")
275+
.imports(&["foopkg"])
276+
.write(workspace.path());
277+
NamespaceWriter::new()
278+
.import("foopkg")
279+
.write(workspace.path());
280+
let use_path = workspace.path().join("R").join("use.R");
281+
let use_uri = Url::from_file_path(&use_path).unwrap();
282+
write_sources(&workspace.path().join("R"), &[("use.R", "foo()\n")]);
283+
284+
// Open the workspace folder, triggering a workspace scan
285+
state
286+
.handle_event_to_quiescence(did_change_workspace_folders(workspace.path()))
287+
.await;
288+
289+
let world = state.world();
290+
let foo_file = world.db.package_by_name("foopkg").unwrap().files(&world.db)[0];
291+
292+
assert_matches!(
293+
goto_definition(make_params(use_uri, 0, 0), world).unwrap(),
294+
Some(GotoDefinitionResponse::Link(ref links)) => {
295+
assert_eq!(links.len(), 1);
296+
assert_eq!(links[0].target_uri, world.wire_url(foo_file));
297+
assert_eq!(links[0].target_range, range((0, 0), (0, 3)));
298+
}
299+
);
300+
}
301+
302+
/// Goto-def from a bare `bar()` through to `barpkg::bar()`'s definition via the
303+
/// workspace package's `importFrom(barpkg, bar)`
304+
#[tokio::test]
305+
async fn test_goto_definition_resolves_unqualified_import_from_into_package() {
306+
let _aux = init_aux_for_test();
307+
308+
let handler = Arc::new(TestSourceHandler::new(HashMap::from([(
309+
String::from("barpkg"),
310+
TestBehavior::Success(vec![("bar.R", "bar <- function() 2\n")]),
311+
)])));
312+
313+
// Set up the library that our "installed" package lives in
314+
let library = tempfile::tempdir().unwrap();
315+
DescriptionWriter::new()
316+
.package("barpkg")
317+
.version("0.0.0")
318+
.built("dummy")
319+
.write(&library.path().join("barpkg"));
320+
NamespaceWriter::new()
321+
.export("bar")
322+
.write(&library.path().join("barpkg"));
323+
324+
let mut db = OakDatabase::new();
325+
db.set_library_paths(&[library.path().to_path_buf()]);
326+
327+
let mut state = GlobalState::from_parts(
328+
test_client(),
329+
WorldState::new(db, Library::new(vec![])),
330+
LspState::new(
331+
tokio::sync::mpsc::unbounded_channel().0,
332+
SourceScheduler::new(Some(handler)),
333+
),
334+
);
335+
336+
// The workspace folder is itself the package that imports `bar`.
337+
let workspace = tempfile::tempdir().unwrap();
338+
DescriptionWriter::new()
339+
.package("mypackage")
340+
.version("0.0.0")
341+
.imports(&["barpkg"])
342+
.write(workspace.path());
343+
NamespaceWriter::new()
344+
.import_from("barpkg", "bar")
345+
.write(workspace.path());
346+
let use_path = workspace.path().join("R").join("use.R");
347+
let use_uri = Url::from_file_path(&use_path).unwrap();
348+
write_sources(&workspace.path().join("R"), &[("use.R", "bar()\n")]);
349+
350+
// Open the workspace folder, triggering a workspace scan
351+
state
352+
.handle_event_to_quiescence(did_change_workspace_folders(workspace.path()))
353+
.await;
354+
355+
let world = state.world();
356+
let bar_file = world.db.package_by_name("barpkg").unwrap().files(&world.db)[0];
357+
358+
assert_matches!(
359+
goto_definition(make_params(use_uri, 0, 0), world).unwrap(),
360+
Some(GotoDefinitionResponse::Link(ref links)) => {
361+
assert_eq!(links.len(), 1);
362+
assert_eq!(links[0].target_uri, world.wire_url(bar_file));
363+
assert_eq!(links[0].target_range, range((0, 0), (0, 3)));
364+
}
365+
);
366+
}

crates/ark/src/lsp/tests/main_loop.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use tower_lsp::lsp_types::WorkspaceFoldersChangeEvent;
1616
use url::Url;
1717

1818
use super::utils::test_client;
19-
use super::utils::write_description;
2019
use super::utils::write_sources;
20+
use super::utils::DescriptionWriter;
2121
use crate::lsp::backend::LspMessage;
2222
use crate::lsp::backend::LspNotification;
2323
use crate::lsp::main_loop::init_aux_for_test;
@@ -44,7 +44,10 @@ async fn test_workspace_folder_scan_drives_through_main_loop() {
4444

4545
let tmp = tempfile::tempdir().unwrap();
4646
let pkg = tmp.path().join("pkg");
47-
write_description(&pkg, "pkg");
47+
DescriptionWriter::new()
48+
.package("pkg")
49+
.version("0.0.0")
50+
.write(&pkg);
4851
write_sources(&pkg.join("R"), &[("a.R", "x <- 1\n")]);
4952

5053
let params = DidChangeWorkspaceFoldersParams {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use std::collections::HashMap;
2+
use std::sync::Mutex;
3+
4+
use super::utils::write_sources;
5+
use crate::lsp::sources::SourceHandler;
6+
use crate::lsp::sources::SourceRequest;
7+
use crate::lsp::sources::SourceResponse;
8+
9+
/// A test [`SourceHandler`] that serves canned behavior per package name and records
10+
/// every call, so tests can count the number of calls. The handler is shared (as the
11+
/// `Arc<dyn SourceHandler>` the `SourceScheduler` holds and the clone the test keeps), so
12+
/// `calls` is a plain `Mutex`.
13+
pub(super) struct TestSourceHandler {
14+
/// Owns the cache directory that `Success` writes per-package sources into.
15+
sources: tempfile::TempDir,
16+
/// Per-package canned behavior.
17+
behavior: HashMap<String, TestBehavior>,
18+
/// Each request passed to `handle`, in call order.
19+
calls: Mutex<Vec<SourceRequest>>,
20+
}
21+
22+
// Canned behavior to perform when a particular package is requested
23+
pub(super) enum TestBehavior {
24+
/// Write these `(basename, contents)` files into the package's source dir
25+
/// and return `Success(dir)`.
26+
Success(Vec<(&'static str, &'static str)>),
27+
Failure,
28+
}
29+
30+
impl TestSourceHandler {
31+
pub(super) fn new(behavior: HashMap<String, TestBehavior>) -> Self {
32+
Self {
33+
sources: tempfile::tempdir().unwrap(),
34+
behavior,
35+
calls: Mutex::new(Vec::new()),
36+
}
37+
}
38+
39+
/// The requests passed to `handle`, in call order, for the test to assert on.
40+
pub(super) fn calls(&self) -> &Mutex<Vec<SourceRequest>> {
41+
&self.calls
42+
}
43+
}
44+
45+
impl SourceHandler for TestSourceHandler {
46+
fn handle(&self, request: &SourceRequest) -> SourceResponse {
47+
self.calls.lock().unwrap().push(request.clone());
48+
49+
match self.behavior.get(request.name()) {
50+
Some(TestBehavior::Success(files)) => {
51+
let dir = self.sources.path().join(request.name());
52+
write_sources(&dir, files);
53+
SourceResponse::Success(dir)
54+
},
55+
Some(TestBehavior::Failure) => SourceResponse::Failure,
56+
None => panic!("Unknown test package {}", request.name()),
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)