Move `ExtensionStore` tests back to `extension_host` (#20682)

Marshall Bowers created

This PR moves the tests for the `ExtensionStore` back into the
`extension_host` crate.

We now have a separate `TestExtensionRegistrationHooks` to use in the
test that implements the minimal required functionality needed for the
tests. This means that we can depend on the `theme` crate only in the
tests.

Release Notes:

- N/A

Change summary

Cargo.lock                                        |  11 -
crates/extension_host/Cargo.toml                  |   1 
crates/extension_host/src/extension_host.rs       |   3 
crates/extension_host/src/extension_store_test.rs | 155 +++++++++++-----
crates/extensions_ui/Cargo.toml                   |  21 --
crates/extensions_ui/src/extensions_ui.rs         |   3 
6 files changed, 106 insertions(+), 88 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4189,6 +4189,7 @@ dependencies = [
  "serde_json_lenient",
  "settings",
  "task",
+ "theme",
  "toml 0.8.19",
  "url",
  "util",
@@ -4203,36 +4204,26 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "assistant_slash_command",
- "async-compression",
- "async-tar",
  "client",
  "collections",
  "context_servers",
- "ctor",
  "db",
  "editor",
- "env_logger 0.11.5",
  "extension",
  "extension_host",
  "fs",
- "futures 0.3.30",
  "fuzzy",
  "gpui",
- "http_client",
  "indexed_docs",
  "language",
  "log",
  "lsp",
- "node_runtime",
  "num-format",
- "parking_lot",
  "picker",
  "project",
  "release_channel",
- "reqwest_client",
  "semantic_version",
  "serde",
- "serde_json",
  "settings",
  "smallvec",
  "snippet_provider",

crates/extension_host/Cargo.toml 🔗

@@ -58,3 +58,4 @@ language = { workspace = true, features = ["test-support"] }
 parking_lot.workspace = true
 project = { workspace = true, features = ["test-support"] }
 reqwest_client.workspace = true
+theme = { workspace = true, features = ["test-support"] }

crates/extension_host/src/extension_host.rs 🔗

@@ -2,6 +2,9 @@ pub mod extension_lsp_adapter;
 pub mod extension_settings;
 pub mod wasm_host;
 
+#[cfg(test)]
+mod extension_store_test;
+
 use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
 use anyhow::{anyhow, bail, Context as _, Result};
 use async_compression::futures::bufread::GzipDecoder;

crates/extensions_ui/src/extension_store_test.rs → crates/extension_host/src/extension_store_test.rs 🔗

@@ -1,20 +1,17 @@
-use assistant_slash_command::SlashCommandRegistry;
-use async_compression::futures::bufread::GzipEncoder;
-use collections::BTreeMap;
-use context_servers::ContextServerFactoryRegistry;
-use extension_host::ExtensionSettings;
-use extension_host::SchemaVersion;
-use extension_host::{
+use crate::extension_lsp_adapter::ExtensionLspAdapter;
+use crate::{
     Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
-    ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
-    RELOAD_DEBOUNCE_DURATION,
+    ExtensionIndexThemeEntry, ExtensionManifest, ExtensionSettings, ExtensionStore,
+    GrammarManifestEntry, SchemaVersion, RELOAD_DEBOUNCE_DURATION,
 };
+use anyhow::Result;
+use async_compression::futures::bufread::GzipEncoder;
+use collections::BTreeMap;
 use fs::{FakeFs, Fs, RealFs};
 use futures::{io::BufReader, AsyncReadExt, StreamExt};
-use gpui::{Context, SemanticVersion, TestAppContext};
+use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext};
 use http_client::{FakeHttpClient, Response};
-use indexed_docs::IndexedDocsRegistry;
-use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus};
+use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
 use lsp::LanguageServerName;
 use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
@@ -23,7 +20,6 @@ use release_channel::AppVersion;
 use reqwest_client::ReqwestClient;
 use serde_json::json;
 use settings::{Settings as _, SettingsStore};
-use snippet_provider::SnippetRegistry;
 use std::{
     ffi::OsString,
     path::{Path, PathBuf},
@@ -32,6 +28,84 @@ use std::{
 use theme::ThemeRegistry;
 use util::test::temp_tree;
 
+use crate::ExtensionRegistrationHooks;
+
+struct TestExtensionRegistrationHooks {
+    executor: BackgroundExecutor,
+    language_registry: Arc<LanguageRegistry>,
+    theme_registry: Arc<ThemeRegistry>,
+}
+
+impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks {
+    fn list_theme_names(&self, path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
+        self.executor.spawn(async move {
+            let themes = theme::read_user_theme(&path, fs).await?;
+            Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
+        })
+    }
+
+    fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
+        let theme_registry = self.theme_registry.clone();
+        self.executor
+            .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
+    }
+
+    fn remove_user_themes(&self, themes: Vec<SharedString>) {
+        self.theme_registry.remove_user_themes(&themes);
+    }
+
+    fn register_language(
+        &self,
+        language: language::LanguageName,
+        grammar: Option<Arc<str>>,
+        matcher: language::LanguageMatcher,
+        load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
+    ) {
+        self.language_registry
+            .register_language(language, grammar, matcher, load)
+    }
+
+    fn remove_languages(
+        &self,
+        languages_to_remove: &[language::LanguageName],
+        grammars_to_remove: &[Arc<str>],
+    ) {
+        self.language_registry
+            .remove_languages(&languages_to_remove, &grammars_to_remove);
+    }
+
+    fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
+        self.language_registry.register_wasm_grammars(grammars)
+    }
+
+    fn register_lsp_adapter(
+        &self,
+        language_name: language::LanguageName,
+        adapter: ExtensionLspAdapter,
+    ) {
+        self.language_registry
+            .register_lsp_adapter(language_name, Arc::new(adapter));
+    }
+
+    fn update_lsp_status(
+        &self,
+        server_name: lsp::LanguageServerName,
+        status: LanguageServerBinaryStatus,
+    ) {
+        self.language_registry
+            .update_lsp_status(server_name, status);
+    }
+
+    fn remove_lsp_adapter(
+        &self,
+        language_name: &language::LanguageName,
+        server_name: &lsp::LanguageServerName,
+    ) {
+        self.language_registry
+            .remove_lsp_adapter(language_name, server_name);
+    }
+}
+
 #[cfg(test)]
 #[ctor::ctor]
 fn init_logger() {
@@ -265,27 +339,18 @@ async fn test_extension_store(cx: &mut TestAppContext) {
 
     let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
     let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
-    let slash_command_registry = SlashCommandRegistry::new();
-    let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
-    let snippet_registry = Arc::new(SnippetRegistry::new());
-    let context_server_factory_registry = cx.new_model(|_| ContextServerFactoryRegistry::new());
+    let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
+        executor: cx.executor(),
+        language_registry: language_registry.clone(),
+        theme_registry: theme_registry.clone(),
+    });
     let node_runtime = NodeRuntime::unavailable();
 
     let store = cx.new_model(|cx| {
-        let extension_registration_hooks = crate::ConcreteExtensionRegistrationHooks::new(
-            theme_registry.clone(),
-            slash_command_registry.clone(),
-            indexed_docs_registry.clone(),
-            snippet_registry.clone(),
-            language_registry.clone(),
-            context_server_factory_registry.clone(),
-            cx,
-        );
-
         ExtensionStore::new(
             PathBuf::from("/the-extension-dir"),
             None,
-            extension_registration_hooks,
+            registration_hooks.clone(),
             fs.clone(),
             http_client.clone(),
             http_client.clone(),
@@ -407,20 +472,10 @@ async fn test_extension_store(cx: &mut TestAppContext) {
     // Create new extension store, as if Zed were restarting.
     drop(store);
     let store = cx.new_model(|cx| {
-        let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
-            theme_registry.clone(),
-            slash_command_registry,
-            indexed_docs_registry,
-            snippet_registry,
-            language_registry.clone(),
-            context_server_factory_registry.clone(),
-            cx,
-        );
-
         ExtensionStore::new(
             PathBuf::from("/the-extension-dir"),
             None,
-            extension_api,
+            registration_hooks,
             fs.clone(),
             http_client.clone(),
             http_client.clone(),
@@ -505,10 +560,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
 
     let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
     let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
-    let slash_command_registry = SlashCommandRegistry::new();
-    let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
-    let snippet_registry = Arc::new(SnippetRegistry::new());
-    let context_server_factory_registry = cx.new_model(|_| ContextServerFactoryRegistry::new());
+    let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
+        executor: cx.executor(),
+        language_registry: language_registry.clone(),
+        theme_registry: theme_registry.clone(),
+    });
     let node_runtime = NodeRuntime::unavailable();
 
     let mut status_updates = language_registry.language_server_binary_statuses();
@@ -599,19 +655,10 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
         Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
 
     let extension_store = cx.new_model(|cx| {
-        let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
-            theme_registry.clone(),
-            slash_command_registry,
-            indexed_docs_registry,
-            snippet_registry,
-            language_registry.clone(),
-            context_server_factory_registry.clone(),
-            cx,
-        );
         ExtensionStore::new(
             extensions_dir.clone(),
             Some(cache_dir),
-            extension_api,
+            registration_hooks,
             fs.clone(),
             extension_client.clone(),
             builder_client,
@@ -626,7 +673,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
     let executor = cx.executor();
     let _task = cx.executor().spawn(async move {
         while let Some(event) = events.next().await {
-            if let extension_host::Event::StartedReloading = event {
+            if let Event::StartedReloading = event {
                 executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
             }
         }

crates/extensions_ui/Cargo.toml 🔗

@@ -11,9 +11,6 @@ workspace = true
 [lib]
 path = "src/extensions_ui.rs"
 
-[features]
-test-support = []
-
 [dependencies]
 anyhow.workspace = true
 assistant_slash_command.workspace = true
@@ -25,7 +22,6 @@ editor.workspace = true
 extension.workspace = true
 extension_host.workspace = true
 fs.workspace = true
-futures.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
 indexed_docs.workspace = true
@@ -50,21 +46,4 @@ wasmtime-wasi.workspace = true
 workspace.workspace = true
 
 [dev-dependencies]
-async-compression.workspace = true
-async-tar.workspace = true
-ctor.workspace = true
 editor = { workspace = true, features = ["test-support"] }
-env_logger.workspace = true
-extension_host = {workspace = true, features = ["test-support"] }
-fs = { workspace = true, features = ["test-support"] }
-gpui = { workspace = true, features = ["test-support"] }
-http_client.workspace = true
-indexed_docs.workspace = true
-language = { workspace = true, features = ["test-support"] }
-lsp.workspace = true
-node_runtime.workspace = true
-parking_lot.workspace = true
-project = { workspace = true, features = ["test-support"] }
-reqwest_client.workspace = true
-serde_json.workspace = true
-workspace = { workspace = true, features = ["test-support"] }

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -3,9 +3,6 @@ mod extension_registration_hooks;
 mod extension_suggest;
 mod extension_version_selector;
 
-#[cfg(test)]
-mod extension_store_test;
-
 pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks;
 
 use std::ops::DerefMut;