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                        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_model(|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_model(|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#[gpui::test]
459async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
460    init_test(cx);
461    cx.executor().allow_parking();
462
463    let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
464        .parent()
465        .unwrap()
466        .parent()
467        .unwrap();
468    let cache_dir = root_dir.join("target");
469    let test_extension_id = "test-extension";
470    let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
471
472    let fs = Arc::new(RealFs::default());
473    let extensions_dir = temp_tree(json!({
474        "installed": {},
475        "work": {}
476    }));
477    let project_dir = temp_tree(json!({
478        "test.gleam": ""
479    }));
480
481    let extensions_dir = extensions_dir.path().canonicalize().unwrap();
482    let project_dir = project_dir.path().canonicalize().unwrap();
483
484    let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
485
486    let proxy = Arc::new(ExtensionHostProxy::new());
487    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
488    theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
489    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
490    language_extension::init(proxy.clone(), language_registry.clone());
491    let node_runtime = NodeRuntime::unavailable();
492
493    let mut status_updates = language_registry.language_server_binary_statuses();
494
495    struct FakeLanguageServerVersion {
496        version: String,
497        binary_contents: String,
498        http_request_count: usize,
499    }
500
501    let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
502        version: "v1.2.3".into(),
503        binary_contents: "the-binary-contents".into(),
504        http_request_count: 0,
505    }));
506
507    let extension_client = FakeHttpClient::create({
508        let language_server_version = language_server_version.clone();
509        move |request| {
510            let language_server_version = language_server_version.clone();
511            async move {
512                let version = language_server_version.lock().version.clone();
513                let binary_contents = language_server_version.lock().binary_contents.clone();
514
515                let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
516                let asset_download_uri =
517                    format!("https://fake-download.example.com/gleam-{version}");
518
519                let uri = request.uri().to_string();
520                if uri == github_releases_uri {
521                    language_server_version.lock().http_request_count += 1;
522                    Ok(Response::new(
523                        json!([
524                            {
525                                "tag_name": version,
526                                "prerelease": false,
527                                "tarball_url": "",
528                                "zipball_url": "",
529                                "assets": [
530                                    {
531                                        "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
532                                        "browser_download_url": asset_download_uri
533                                    },
534                                    {
535                                        "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
536                                        "browser_download_url": asset_download_uri
537                                    },
538                                    {
539                                        "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
540                                        "browser_download_url": asset_download_uri
541                                    }
542                                ]
543                            }
544                        ])
545                        .to_string()
546                        .into(),
547                    ))
548                } else if uri == asset_download_uri {
549                    language_server_version.lock().http_request_count += 1;
550                    let mut bytes = Vec::<u8>::new();
551                    let mut archive = async_tar::Builder::new(&mut bytes);
552                    let mut header = async_tar::Header::new_gnu();
553                    header.set_size(binary_contents.len() as u64);
554                    archive
555                        .append_data(&mut header, "gleam", binary_contents.as_bytes())
556                        .await
557                        .unwrap();
558                    archive.into_inner().await.unwrap();
559                    let mut gzipped_bytes = Vec::new();
560                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
561                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
562                    Ok(Response::new(gzipped_bytes.into()))
563                } else {
564                    Ok(Response::builder().status(404).body("not found".into())?)
565                }
566            }
567        }
568    });
569    let user_agent = cx.update(|cx| {
570        format!(
571            "Zed/{} ({}; {})",
572            AppVersion::global(cx),
573            std::env::consts::OS,
574            std::env::consts::ARCH
575        )
576    });
577    let builder_client =
578        Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
579
580    let extension_store = cx.new_model(|cx| {
581        ExtensionStore::new(
582            extensions_dir.clone(),
583            Some(cache_dir),
584            proxy,
585            fs.clone(),
586            extension_client.clone(),
587            builder_client,
588            None,
589            node_runtime,
590            cx,
591        )
592    });
593
594    // Ensure that debounces fire.
595    let mut events = cx.events(&extension_store);
596    let executor = cx.executor();
597    let _task = cx.executor().spawn(async move {
598        while let Some(event) = events.next().await {
599            if let Event::StartedReloading = event {
600                executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
601            }
602        }
603    });
604
605    extension_store.update(cx, |_, cx| {
606        cx.subscribe(&extension_store, |_, _, event, _| {
607            if matches!(event, Event::ExtensionFailedToLoad(_)) {
608                panic!("extension failed to load");
609            }
610        })
611        .detach();
612    });
613
614    extension_store
615        .update(cx, |store, cx| {
616            store.install_dev_extension(test_extension_dir.clone(), cx)
617        })
618        .await
619        .unwrap();
620
621    let mut fake_servers = language_registry.register_fake_language_server(
622        LanguageServerName("gleam".into()),
623        lsp::ServerCapabilities {
624            completion_provider: Some(Default::default()),
625            ..Default::default()
626        },
627        None,
628    );
629
630    let (buffer, _handle) = project
631        .update(cx, |project, cx| {
632            project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx)
633        })
634        .await
635        .unwrap();
636
637    let fake_server = fake_servers.next().await.unwrap();
638    let expected_server_path =
639        extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam"));
640    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
641
642    assert_eq!(fake_server.binary.path, expected_server_path);
643    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
644    assert_eq!(
645        fs.load(&expected_server_path).await.unwrap(),
646        expected_binary_contents
647    );
648    assert_eq!(language_server_version.lock().http_request_count, 2);
649    assert_eq!(
650        [
651            status_updates.next().await.unwrap(),
652            status_updates.next().await.unwrap(),
653            status_updates.next().await.unwrap(),
654        ],
655        [
656            (
657                LanguageServerName("gleam".into()),
658                LanguageServerBinaryStatus::CheckingForUpdate
659            ),
660            (
661                LanguageServerName("gleam".into()),
662                LanguageServerBinaryStatus::Downloading
663            ),
664            (
665                LanguageServerName("gleam".into()),
666                LanguageServerBinaryStatus::None
667            )
668        ]
669    );
670
671    // The extension creates custom labels for completion items.
672    fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
673        Ok(Some(lsp::CompletionResponse::Array(vec![
674            lsp::CompletionItem {
675                label: "foo".into(),
676                kind: Some(lsp::CompletionItemKind::FUNCTION),
677                detail: Some("fn() -> Result(Nil, Error)".into()),
678                ..Default::default()
679            },
680            lsp::CompletionItem {
681                label: "bar.baz".into(),
682                kind: Some(lsp::CompletionItemKind::FUNCTION),
683                detail: Some("fn(List(a)) -> a".into()),
684                ..Default::default()
685            },
686            lsp::CompletionItem {
687                label: "Quux".into(),
688                kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
689                detail: Some("fn(String) -> T".into()),
690                ..Default::default()
691            },
692            lsp::CompletionItem {
693                label: "my_string".into(),
694                kind: Some(lsp::CompletionItemKind::CONSTANT),
695                detail: Some("String".into()),
696                ..Default::default()
697            },
698        ])))
699    });
700
701    let completion_labels = project
702        .update(cx, |project, cx| {
703            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
704        })
705        .await
706        .unwrap()
707        .into_iter()
708        .map(|c| c.label.text)
709        .collect::<Vec<_>>();
710    assert_eq!(
711        completion_labels,
712        [
713            "foo: fn() -> Result(Nil, Error)".to_string(),
714            "bar.baz: fn(List(a)) -> a".to_string(),
715            "Quux: fn(String) -> T".to_string(),
716            "my_string: String".to_string(),
717        ]
718    );
719
720    // Simulate a new version of the language server being released
721    language_server_version.lock().version = "v2.0.0".into();
722    language_server_version.lock().binary_contents = "the-new-binary-contents".into();
723    language_server_version.lock().http_request_count = 0;
724
725    // Start a new instance of the language server.
726    project.update(cx, |project, cx| {
727        project.restart_language_servers_for_buffers([buffer.clone()], cx)
728    });
729
730    // The extension has cached the binary path, and does not attempt
731    // to reinstall it.
732    let fake_server = fake_servers.next().await.unwrap();
733    assert_eq!(fake_server.binary.path, expected_server_path);
734    assert_eq!(
735        fs.load(&expected_server_path).await.unwrap(),
736        expected_binary_contents
737    );
738    assert_eq!(language_server_version.lock().http_request_count, 0);
739
740    // Reload the extension, clearing its cache.
741    // Start a new instance of the language server.
742    extension_store
743        .update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
744        .await;
745
746    cx.executor().run_until_parked();
747    project.update(cx, |project, cx| {
748        project.restart_language_servers_for_buffers([buffer.clone()], cx)
749    });
750
751    // The extension re-fetches the latest version of the language server.
752    let fake_server = fake_servers.next().await.unwrap();
753    let new_expected_server_path =
754        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
755    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
756    assert_eq!(fake_server.binary.path, new_expected_server_path);
757    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
758    assert_eq!(
759        fs.load(&new_expected_server_path).await.unwrap(),
760        expected_binary_contents
761    );
762
763    // The old language server directory has been cleaned up.
764    assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
765}
766
767fn init_test(cx: &mut TestAppContext) {
768    cx.update(|cx| {
769        let store = SettingsStore::test(cx);
770        cx.set_global(store);
771        release_channel::init(SemanticVersion::default(), cx);
772        theme::init(theme::LoadThemes::JustBase, cx);
773        Project::init_settings(cx);
774        ExtensionSettings::register(cx);
775        language::init(cx);
776    });
777}