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