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