extension_store_test.rs

  1use crate::{
  2    Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
  3    ExtensionIndexThemeEntry, ExtensionManifest, ExtensionSettings, ExtensionStore,
  4    GrammarManifestEntry, SchemaVersion, RELOAD_DEBOUNCE_DURATION,
  5};
  6use async_compression::futures::bufread::GzipEncoder;
  7use collections::BTreeMap;
  8use extension::ExtensionHostProxy;
  9use fs::{FakeFs, Fs, RealFs};
 10use futures::{io::BufReader, AsyncReadExt, StreamExt};
 11use gpui::{Context, SemanticVersion, TestAppContext};
 12use http_client::{FakeHttpClient, Response};
 13use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus};
 14use lsp::LanguageServerName;
 15use node_runtime::NodeRuntime;
 16use parking_lot::Mutex;
 17use project::{Project, DEFAULT_COMPLETION_CONTEXT};
 18use release_channel::AppVersion;
 19use reqwest_client::ReqwestClient;
 20use serde_json::json;
 21use settings::{Settings as _, SettingsStore};
 22use std::{
 23    ffi::OsString,
 24    path::{Path, PathBuf},
 25    sync::Arc,
 26};
 27use theme::ThemeRegistry;
 28use util::test::temp_tree;
 29
 30#[cfg(test)]
 31#[ctor::ctor]
 32fn init_logger() {
 33    if std::env::var("RUST_LOG").is_ok() {
 34        env_logger::init();
 35    }
 36}
 37
 38#[gpui::test]
 39async fn test_extension_store(cx: &mut TestAppContext) {
 40    init_test(cx);
 41
 42    let fs = FakeFs::new(cx.executor());
 43    let http_client = FakeHttpClient::with_200_response();
 44
 45    fs.insert_tree(
 46        "/the-extension-dir",
 47        json!({
 48            "installed": {
 49                "zed-monokai": {
 50                    "extension.json": r#"{
 51                        "id": "zed-monokai",
 52                        "name": "Zed Monokai",
 53                        "version": "2.0.0",
 54                        "themes": {
 55                            "Monokai Dark": "themes/monokai.json",
 56                            "Monokai Light": "themes/monokai.json",
 57                            "Monokai Pro Dark": "themes/monokai-pro.json",
 58                            "Monokai Pro Light": "themes/monokai-pro.json"
 59                        }
 60                    }"#,
 61                    "themes": {
 62                        "monokai.json": r#"{
 63                            "name": "Monokai",
 64                            "author": "Someone",
 65                            "themes": [
 66                                {
 67                                    "name": "Monokai Dark",
 68                                    "appearance": "dark",
 69                                    "style": {}
 70                                },
 71                                {
 72                                    "name": "Monokai Light",
 73                                    "appearance": "light",
 74                                    "style": {}
 75                                }
 76                            ]
 77                        }"#,
 78                        "monokai-pro.json": r#"{
 79                            "name": "Monokai Pro",
 80                            "author": "Someone",
 81                            "themes": [
 82                                {
 83                                    "name": "Monokai Pro Dark",
 84                                    "appearance": "dark",
 85                                    "style": {}
 86                                },
 87                                {
 88                                    "name": "Monokai Pro Light",
 89                                    "appearance": "light",
 90                                    "style": {}
 91                                }
 92                            ]
 93                        }"#,
 94                    }
 95                },
 96                "zed-ruby": {
 97                    "extension.json": r#"{
 98                        "id": "zed-ruby",
 99                        "name": "Zed Ruby",
100                        "version": "1.0.0",
101                        "grammars": {
102                            "ruby": "grammars/ruby.wasm",
103                            "embedded_template": "grammars/embedded_template.wasm"
104                        },
105                        "languages": {
106                            "ruby": "languages/ruby",
107                            "erb": "languages/erb"
108                        }
109                    }"#,
110                    "grammars": {
111                        "ruby.wasm": "",
112                        "embedded_template.wasm": "",
113                    },
114                    "languages": {
115                        "ruby": {
116                            "config.toml": r#"
117                                name = "Ruby"
118                                grammar = "ruby"
119                                path_suffixes = ["rb"]
120                            "#,
121                            "highlights.scm": "",
122                        },
123                        "erb": {
124                            "config.toml": r#"
125                                name = "ERB"
126                                grammar = "embedded_template"
127                                path_suffixes = ["erb"]
128                            "#,
129                            "highlights.scm": "",
130                        }
131                    },
132                }
133            }
134        }),
135    )
136    .await;
137
138    let mut expected_index = ExtensionIndex {
139        extensions: [
140            (
141                "zed-ruby".into(),
142                ExtensionIndexEntry {
143                    manifest: Arc::new(ExtensionManifest {
144                        id: "zed-ruby".into(),
145                        name: "Zed Ruby".into(),
146                        version: "1.0.0".into(),
147                        schema_version: SchemaVersion::ZERO,
148                        description: None,
149                        authors: Vec::new(),
150                        repository: None,
151                        themes: Default::default(),
152                        lib: Default::default(),
153                        languages: vec!["languages/erb".into(), "languages/ruby".into()],
154                        grammars: [
155                            ("embedded_template".into(), GrammarManifestEntry::default()),
156                            ("ruby".into(), GrammarManifestEntry::default()),
157                        ]
158                        .into_iter()
159                        .collect(),
160                        language_servers: BTreeMap::default(),
161                        context_servers: BTreeMap::default(),
162                        slash_commands: BTreeMap::default(),
163                        indexed_docs_providers: BTreeMap::default(),
164                        snippets: None,
165                    }),
166                    dev: false,
167                },
168            ),
169            (
170                "zed-monokai".into(),
171                ExtensionIndexEntry {
172                    manifest: Arc::new(ExtensionManifest {
173                        id: "zed-monokai".into(),
174                        name: "Zed Monokai".into(),
175                        version: "2.0.0".into(),
176                        schema_version: SchemaVersion::ZERO,
177                        description: None,
178                        authors: vec![],
179                        repository: None,
180                        themes: vec![
181                            "themes/monokai-pro.json".into(),
182                            "themes/monokai.json".into(),
183                        ],
184                        lib: Default::default(),
185                        languages: Default::default(),
186                        grammars: BTreeMap::default(),
187                        language_servers: BTreeMap::default(),
188                        context_servers: BTreeMap::default(),
189                        slash_commands: BTreeMap::default(),
190                        indexed_docs_providers: BTreeMap::default(),
191                        snippets: None,
192                    }),
193                    dev: false,
194                },
195            ),
196        ]
197        .into_iter()
198        .collect(),
199        languages: [
200            (
201                "ERB".into(),
202                ExtensionIndexLanguageEntry {
203                    extension: "zed-ruby".into(),
204                    path: "languages/erb".into(),
205                    grammar: Some("embedded_template".into()),
206                    matcher: LanguageMatcher {
207                        path_suffixes: vec!["erb".into()],
208                        first_line_pattern: None,
209                    },
210                },
211            ),
212            (
213                "Ruby".into(),
214                ExtensionIndexLanguageEntry {
215                    extension: "zed-ruby".into(),
216                    path: "languages/ruby".into(),
217                    grammar: Some("ruby".into()),
218                    matcher: LanguageMatcher {
219                        path_suffixes: vec!["rb".into()],
220                        first_line_pattern: None,
221                    },
222                },
223            ),
224        ]
225        .into_iter()
226        .collect(),
227        themes: [
228            (
229                "Monokai Dark".into(),
230                ExtensionIndexThemeEntry {
231                    extension: "zed-monokai".into(),
232                    path: "themes/monokai.json".into(),
233                },
234            ),
235            (
236                "Monokai Light".into(),
237                ExtensionIndexThemeEntry {
238                    extension: "zed-monokai".into(),
239                    path: "themes/monokai.json".into(),
240                },
241            ),
242            (
243                "Monokai Pro Dark".into(),
244                ExtensionIndexThemeEntry {
245                    extension: "zed-monokai".into(),
246                    path: "themes/monokai-pro.json".into(),
247                },
248            ),
249            (
250                "Monokai Pro Light".into(),
251                ExtensionIndexThemeEntry {
252                    extension: "zed-monokai".into(),
253                    path: "themes/monokai-pro.json".into(),
254                },
255            ),
256        ]
257        .into_iter()
258        .collect(),
259    };
260
261    let proxy = Arc::new(ExtensionHostProxy::new());
262    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
263    theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
264    let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
265    language_extension::init(proxy.clone(), language_registry.clone());
266    let node_runtime = NodeRuntime::unavailable();
267
268    let store = cx.new_model(|cx| {
269        ExtensionStore::new(
270            PathBuf::from("/the-extension-dir"),
271            None,
272            proxy.clone(),
273            fs.clone(),
274            http_client.clone(),
275            http_client.clone(),
276            None,
277            node_runtime.clone(),
278            cx,
279        )
280    });
281
282    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
283    store.read_with(cx, |store, _| {
284        let index = &store.extension_index;
285        assert_eq!(index.extensions, expected_index.extensions);
286        assert_eq!(index.languages, expected_index.languages);
287        assert_eq!(index.themes, expected_index.themes);
288
289        assert_eq!(
290            language_registry.language_names(),
291            ["ERB", "Plain Text", "Ruby"]
292        );
293        assert_eq!(
294            theme_registry.list_names(),
295            [
296                "Monokai Dark",
297                "Monokai Light",
298                "Monokai Pro Dark",
299                "Monokai Pro Light",
300                "One Dark",
301            ]
302        );
303    });
304
305    fs.insert_tree(
306        "/the-extension-dir/installed/zed-gruvbox",
307        json!({
308            "extension.json": r#"{
309                "id": "zed-gruvbox",
310                "name": "Zed Gruvbox",
311                "version": "1.0.0",
312                "themes": {
313                    "Gruvbox": "themes/gruvbox.json"
314                }
315            }"#,
316            "themes": {
317                "gruvbox.json": r#"{
318                    "name": "Gruvbox",
319                    "author": "Someone Else",
320                    "themes": [
321                        {
322                            "name": "Gruvbox",
323                            "appearance": "dark",
324                            "style": {}
325                        }
326                    ]
327                }"#,
328            }
329        }),
330    )
331    .await;
332
333    expected_index.extensions.insert(
334        "zed-gruvbox".into(),
335        ExtensionIndexEntry {
336            manifest: Arc::new(ExtensionManifest {
337                id: "zed-gruvbox".into(),
338                name: "Zed Gruvbox".into(),
339                version: "1.0.0".into(),
340                schema_version: SchemaVersion::ZERO,
341                description: None,
342                authors: vec![],
343                repository: None,
344                themes: vec!["themes/gruvbox.json".into()],
345                lib: Default::default(),
346                languages: Default::default(),
347                grammars: BTreeMap::default(),
348                language_servers: BTreeMap::default(),
349                context_servers: BTreeMap::default(),
350                slash_commands: BTreeMap::default(),
351                indexed_docs_providers: BTreeMap::default(),
352                snippets: None,
353            }),
354            dev: false,
355        },
356    );
357    expected_index.themes.insert(
358        "Gruvbox".into(),
359        ExtensionIndexThemeEntry {
360            extension: "zed-gruvbox".into(),
361            path: "themes/gruvbox.json".into(),
362        },
363    );
364
365    #[allow(clippy::let_underscore_future)]
366    let _ = store.update(cx, |store, cx| store.reload(None, cx));
367
368    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
369    store.read_with(cx, |store, _| {
370        let index = &store.extension_index;
371        assert_eq!(index.extensions, expected_index.extensions);
372        assert_eq!(index.languages, expected_index.languages);
373        assert_eq!(index.themes, expected_index.themes);
374
375        assert_eq!(
376            theme_registry.list_names(),
377            [
378                "Gruvbox",
379                "Monokai Dark",
380                "Monokai Light",
381                "Monokai Pro Dark",
382                "Monokai Pro Light",
383                "One Dark",
384            ]
385        );
386    });
387
388    let prev_fs_metadata_call_count = fs.metadata_call_count();
389    let prev_fs_read_dir_call_count = fs.read_dir_call_count();
390
391    // Create new extension store, as if Zed were restarting.
392    drop(store);
393    let store = cx.new_model(|cx| {
394        ExtensionStore::new(
395            PathBuf::from("/the-extension-dir"),
396            None,
397            proxy,
398            fs.clone(),
399            http_client.clone(),
400            http_client.clone(),
401            None,
402            node_runtime.clone(),
403            cx,
404        )
405    });
406
407    cx.executor().run_until_parked();
408    store.read_with(cx, |store, _| {
409        assert_eq!(store.extension_index, expected_index);
410        assert_eq!(
411            language_registry.language_names(),
412            ["ERB", "Plain Text", "Ruby"]
413        );
414        assert_eq!(
415            language_registry.grammar_names(),
416            ["embedded_template".into(), "ruby".into()]
417        );
418        assert_eq!(
419            theme_registry.list_names(),
420            [
421                "Gruvbox",
422                "Monokai Dark",
423                "Monokai Light",
424                "Monokai Pro Dark",
425                "Monokai Pro Light",
426                "One Dark",
427            ]
428        );
429
430        // The on-disk manifest limits the number of FS calls that need to be made
431        // on startup.
432        assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
433        assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
434    });
435
436    store.update(cx, |store, cx| {
437        store.uninstall_extension("zed-ruby".into(), cx)
438    });
439
440    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
441    expected_index.extensions.remove("zed-ruby");
442    expected_index.languages.remove("Ruby");
443    expected_index.languages.remove("ERB");
444
445    store.read_with(cx, |store, _| {
446        assert_eq!(store.extension_index, expected_index);
447        assert_eq!(language_registry.language_names(), ["Plain Text"]);
448        assert_eq!(language_registry.grammar_names(), []);
449    });
450}
451
452#[gpui::test]
453async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
454    init_test(cx);
455    cx.executor().allow_parking();
456
457    let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
458        .parent()
459        .unwrap()
460        .parent()
461        .unwrap();
462    let cache_dir = root_dir.join("target");
463    let test_extension_id = "test-extension";
464    let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
465
466    let fs = Arc::new(RealFs::default());
467    let extensions_dir = temp_tree(json!({
468        "installed": {},
469        "work": {}
470    }));
471    let project_dir = temp_tree(json!({
472        "test.gleam": ""
473    }));
474
475    let extensions_dir = extensions_dir.path().canonicalize().unwrap();
476    let project_dir = project_dir.path().canonicalize().unwrap();
477
478    let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
479
480    let proxy = Arc::new(ExtensionHostProxy::new());
481    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
482    theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
483    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
484    language_extension::init(proxy.clone(), language_registry.clone());
485    let node_runtime = NodeRuntime::unavailable();
486
487    let mut status_updates = language_registry.language_server_binary_statuses();
488
489    struct FakeLanguageServerVersion {
490        version: String,
491        binary_contents: String,
492        http_request_count: usize,
493    }
494
495    let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
496        version: "v1.2.3".into(),
497        binary_contents: "the-binary-contents".into(),
498        http_request_count: 0,
499    }));
500
501    let extension_client = FakeHttpClient::create({
502        let language_server_version = language_server_version.clone();
503        move |request| {
504            let language_server_version = language_server_version.clone();
505            async move {
506                let version = language_server_version.lock().version.clone();
507                let binary_contents = language_server_version.lock().binary_contents.clone();
508
509                let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
510                let asset_download_uri =
511                    format!("https://fake-download.example.com/gleam-{version}");
512
513                let uri = request.uri().to_string();
514                if uri == github_releases_uri {
515                    language_server_version.lock().http_request_count += 1;
516                    Ok(Response::new(
517                        json!([
518                            {
519                                "tag_name": version,
520                                "prerelease": false,
521                                "tarball_url": "",
522                                "zipball_url": "",
523                                "assets": [
524                                    {
525                                        "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
526                                        "browser_download_url": asset_download_uri
527                                    },
528                                    {
529                                        "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
530                                        "browser_download_url": asset_download_uri
531                                    },
532                                    {
533                                        "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
534                                        "browser_download_url": asset_download_uri
535                                    }
536                                ]
537                            }
538                        ])
539                        .to_string()
540                        .into(),
541                    ))
542                } else if uri == asset_download_uri {
543                    language_server_version.lock().http_request_count += 1;
544                    let mut bytes = Vec::<u8>::new();
545                    let mut archive = async_tar::Builder::new(&mut bytes);
546                    let mut header = async_tar::Header::new_gnu();
547                    header.set_size(binary_contents.len() as u64);
548                    archive
549                        .append_data(&mut header, "gleam", binary_contents.as_bytes())
550                        .await
551                        .unwrap();
552                    archive.into_inner().await.unwrap();
553                    let mut gzipped_bytes = Vec::new();
554                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
555                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
556                    Ok(Response::new(gzipped_bytes.into()))
557                } else {
558                    Ok(Response::builder().status(404).body("not found".into())?)
559                }
560            }
561        }
562    });
563    let user_agent = cx.update(|cx| {
564        format!(
565            "Zed/{} ({}; {})",
566            AppVersion::global(cx),
567            std::env::consts::OS,
568            std::env::consts::ARCH
569        )
570    });
571    let builder_client =
572        Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
573
574    let extension_store = cx.new_model(|cx| {
575        ExtensionStore::new(
576            extensions_dir.clone(),
577            Some(cache_dir),
578            proxy,
579            fs.clone(),
580            extension_client.clone(),
581            builder_client,
582            None,
583            node_runtime,
584            cx,
585        )
586    });
587
588    // Ensure that debounces fire.
589    let mut events = cx.events(&extension_store);
590    let executor = cx.executor();
591    let _task = cx.executor().spawn(async move {
592        while let Some(event) = events.next().await {
593            if let Event::StartedReloading = event {
594                executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
595            }
596        }
597    });
598
599    extension_store.update(cx, |_, cx| {
600        cx.subscribe(&extension_store, |_, _, event, _| {
601            if matches!(event, Event::ExtensionFailedToLoad(_)) {
602                panic!("extension failed to load");
603            }
604        })
605        .detach();
606    });
607
608    extension_store
609        .update(cx, |store, cx| {
610            store.install_dev_extension(test_extension_dir.clone(), cx)
611        })
612        .await
613        .unwrap();
614
615    let mut fake_servers = language_registry.register_fake_language_server(
616        LanguageServerName("gleam".into()),
617        lsp::ServerCapabilities {
618            completion_provider: Some(Default::default()),
619            ..Default::default()
620        },
621        None,
622    );
623
624    let buffer = project
625        .update(cx, |project, cx| {
626            project.open_local_buffer(project_dir.join("test.gleam"), cx)
627        })
628        .await
629        .unwrap();
630
631    let fake_server = fake_servers.next().await.unwrap();
632    let expected_server_path =
633        extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam"));
634    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
635
636    assert_eq!(fake_server.binary.path, expected_server_path);
637    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
638    assert_eq!(
639        fs.load(&expected_server_path).await.unwrap(),
640        expected_binary_contents
641    );
642    assert_eq!(language_server_version.lock().http_request_count, 2);
643    assert_eq!(
644        [
645            status_updates.next().await.unwrap(),
646            status_updates.next().await.unwrap(),
647            status_updates.next().await.unwrap(),
648        ],
649        [
650            (
651                LanguageServerName("gleam".into()),
652                LanguageServerBinaryStatus::CheckingForUpdate
653            ),
654            (
655                LanguageServerName("gleam".into()),
656                LanguageServerBinaryStatus::Downloading
657            ),
658            (
659                LanguageServerName("gleam".into()),
660                LanguageServerBinaryStatus::None
661            )
662        ]
663    );
664
665    // The extension creates custom labels for completion items.
666    fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
667        Ok(Some(lsp::CompletionResponse::Array(vec![
668            lsp::CompletionItem {
669                label: "foo".into(),
670                kind: Some(lsp::CompletionItemKind::FUNCTION),
671                detail: Some("fn() -> Result(Nil, Error)".into()),
672                ..Default::default()
673            },
674            lsp::CompletionItem {
675                label: "bar.baz".into(),
676                kind: Some(lsp::CompletionItemKind::FUNCTION),
677                detail: Some("fn(List(a)) -> a".into()),
678                ..Default::default()
679            },
680            lsp::CompletionItem {
681                label: "Quux".into(),
682                kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
683                detail: Some("fn(String) -> T".into()),
684                ..Default::default()
685            },
686            lsp::CompletionItem {
687                label: "my_string".into(),
688                kind: Some(lsp::CompletionItemKind::CONSTANT),
689                detail: Some("String".into()),
690                ..Default::default()
691            },
692        ])))
693    });
694
695    let completion_labels = project
696        .update(cx, |project, cx| {
697            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
698        })
699        .await
700        .unwrap()
701        .into_iter()
702        .map(|c| c.label.text)
703        .collect::<Vec<_>>();
704    assert_eq!(
705        completion_labels,
706        [
707            "foo: fn() -> Result(Nil, Error)".to_string(),
708            "bar.baz: fn(List(a)) -> a".to_string(),
709            "Quux: fn(String) -> T".to_string(),
710            "my_string: String".to_string(),
711        ]
712    );
713
714    // Simulate a new version of the language server being released
715    language_server_version.lock().version = "v2.0.0".into();
716    language_server_version.lock().binary_contents = "the-new-binary-contents".into();
717    language_server_version.lock().http_request_count = 0;
718
719    // Start a new instance of the language server.
720    project.update(cx, |project, cx| {
721        project.restart_language_servers_for_buffers([buffer.clone()], cx)
722    });
723
724    // The extension has cached the binary path, and does not attempt
725    // to reinstall it.
726    let fake_server = fake_servers.next().await.unwrap();
727    assert_eq!(fake_server.binary.path, expected_server_path);
728    assert_eq!(
729        fs.load(&expected_server_path).await.unwrap(),
730        expected_binary_contents
731    );
732    assert_eq!(language_server_version.lock().http_request_count, 0);
733
734    // Reload the extension, clearing its cache.
735    // Start a new instance of the language server.
736    extension_store
737        .update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
738        .await;
739
740    cx.executor().run_until_parked();
741    project.update(cx, |project, cx| {
742        project.restart_language_servers_for_buffers([buffer.clone()], cx)
743    });
744
745    // The extension re-fetches the latest version of the language server.
746    let fake_server = fake_servers.next().await.unwrap();
747    let new_expected_server_path =
748        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
749    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
750    assert_eq!(fake_server.binary.path, new_expected_server_path);
751    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
752    assert_eq!(
753        fs.load(&new_expected_server_path).await.unwrap(),
754        expected_binary_contents
755    );
756
757    // The old language server directory has been cleaned up.
758    assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
759}
760
761fn init_test(cx: &mut TestAppContext) {
762    cx.update(|cx| {
763        let store = SettingsStore::test(cx);
764        cx.set_global(store);
765        release_channel::init(SemanticVersion::default(), cx);
766        theme::init(theme::LoadThemes::JustBase, cx);
767        Project::init_settings(cx);
768        ExtensionSettings::register(cx);
769        language::init(cx);
770    });
771}