Add unit test for project symbols that demonstrates crash

This commit is contained in:
Max Brunsfeld 2022-04-20 15:07:05 -07:00
parent 09634dffb8
commit 84df1d6564
4 changed files with 150 additions and 5 deletions

3
Cargo.lock generated
View File

@ -3975,8 +3975,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"editor", "editor",
"futures",
"fuzzy", "fuzzy",
"gpui", "gpui",
"language",
"lsp",
"ordered-float", "ordered-float",
"picker", "picker",
"postage", "postage",

View File

@ -139,7 +139,7 @@ impl<D: PickerDelegate> Picker<D> {
max_size: vec2f(540., 420.), max_size: vec2f(540., 420.),
confirmed: false, confirmed: false,
}; };
cx.defer(|this, cx| this.update_matches(cx)); cx.defer(|this, cx| this.update_matches(String::new(), cx));
this this
} }
@ -159,7 +159,7 @@ impl<D: PickerDelegate> Picker<D> {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
editor::Event::BufferEdited { .. } => self.update_matches(cx), editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx),
editor::Event::Blurred if !self.confirmed => { editor::Event::Blurred if !self.confirmed => {
if let Some(delegate) = self.delegate.upgrade(cx) { if let Some(delegate) = self.delegate.upgrade(cx) {
delegate.update(cx, |delegate, cx| { delegate.update(cx, |delegate, cx| {
@ -171,9 +171,8 @@ impl<D: PickerDelegate> Picker<D> {
} }
} }
fn update_matches(&mut self, cx: &mut ViewContext<Self>) { pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) { if let Some(delegate) = self.delegate.upgrade(cx) {
let query = self.query(cx);
let update = delegate.update(cx, |d, cx| d.update_matches(query, cx)); let update = delegate.update(cx, |d, cx| d.update_matches(query, cx));
cx.notify(); cx.notify();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {

View File

@ -21,3 +21,11 @@ anyhow = "1.0.38"
ordered-float = "2.1.1" ordered-float = "2.1.1"
postage = { version = "0.4", features = ["futures-traits"] } postage = { version = "0.4", features = ["futures-traits"] }
smol = "1.2" smol = "1.2"
[dev-dependencies]
futures = "0.3"
settings = { path = "../settings", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }

View File

@ -90,7 +90,7 @@ impl ProjectSymbolsView {
}) })
.collect() .collect()
} else { } else {
smol::block_on(fuzzy::match_strings( cx.background_executor().block(fuzzy::match_strings(
&self.match_candidates, &self.match_candidates,
query, query,
false, false,
@ -263,3 +263,138 @@ impl PickerDelegate for ProjectSymbolsView {
.boxed() .boxed()
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use futures::StreamExt;
use gpui::{serde_json::json, TestAppContext};
use language::{FakeLspAdapter, Language, LanguageConfig};
use project::FakeFs;
use std::sync::Arc;
#[gpui::test]
async fn test_project_symbols(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
cx.update(|cx| cx.set_global(Settings::test(cx)));
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
None,
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter::default());
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
let project = Project::test(fs.clone(), cx);
project.update(cx, |project, _| {
project.languages().add(Arc::new(language));
});
let worktree_id = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/dir", true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |tree, _| tree.id());
let _buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "test.rs"), cx)
})
.await
.unwrap();
// Set up fake langauge server to return fuzzy matches against
// a fixed set of symbol names.
let fake_symbol_names = ["one", "ton", "uno"];
let fake_server = fake_servers.next().await.unwrap();
fake_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(
move |params: lsp::WorkspaceSymbolParams, cx| {
let executor = cx.background();
async move {
let candidates = fake_symbol_names
.into_iter()
.map(|name| StringMatchCandidate::new(0, name.into()))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&params.query,
true,
100,
&Default::default(),
executor.clone(),
)
.await;
Ok(Some(
matches.into_iter().map(|mat| symbol(&mat.string)).collect(),
))
}
},
);
// Create the project symbols view.
let (_, symbols_view) = cx.add_window(|cx| ProjectSymbolsView::new(project.clone(), cx));
let picker = symbols_view.read_with(cx, |symbols_view, _| symbols_view.picker.clone());
// Spawn multiples updates before the first update completes,
// such that in the end, there are no matches. Testing for regression:
// https://github.com/zed-industries/zed/issues/861
picker.update(cx, |p, cx| {
p.update_matches("o".to_string(), cx);
p.update_matches("on".to_string(), cx);
p.update_matches("onex".to_string(), cx);
});
cx.foreground().run_until_parked();
symbols_view.read_with(cx, |symbols_view, _| {
assert_eq!(symbols_view.matches.len(), 0);
});
// Spawn more updates such that in the end, there are matches.
picker.update(cx, |p, cx| {
p.update_matches("one".to_string(), cx);
p.update_matches("on".to_string(), cx);
});
cx.foreground().run_until_parked();
symbols_view.read_with(cx, |symbols_view, _| {
assert_eq!(symbols_view.matches.len(), 2);
assert_eq!(symbols_view.matches[0].string, "one");
assert_eq!(symbols_view.matches[1].string, "ton");
});
// Spawn more updates such that in the end, there are again no matches.
picker.update(cx, |p, cx| {
p.update_matches("o".to_string(), cx);
p.update_matches("".to_string(), cx);
});
cx.foreground().run_until_parked();
symbols_view.read_with(cx, |symbols_view, _| {
assert_eq!(symbols_view.matches.len(), 0);
});
}
fn symbol(name: &str) -> lsp::SymbolInformation {
#[allow(deprecated)]
lsp::SymbolInformation {
name: name.to_string(),
kind: lsp::SymbolKind::FUNCTION,
tags: None,
deprecated: None,
container_name: None,
location: lsp::Location::new(
lsp::Url::from_file_path("/a/b").unwrap(),
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
),
}
}
}