extension_store_test.rs

  1use crate::extension_builder::WASI_ADAPTER_URL;
  2use crate::extension_manifest::SchemaVersion;
  3use crate::extension_settings::ExtensionSettings;
  4use crate::{
  5    Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
  6    ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
  7    RELOAD_DEBOUNCE_DURATION,
  8};
  9use assistant_slash_command::SlashCommandRegistry;
 10use async_compression::futures::bufread::GzipEncoder;
 11use collections::BTreeMap;
 12use fs::{FakeFs, Fs, RealFs};
 13use futures::{io::BufReader, AsyncReadExt, StreamExt};
 14use gpui::{Context, SemanticVersion, TestAppContext};
 15use http_client::{AsyncBody, FakeHttpClient, HttpClient, Response};
 16use indexed_docs::IndexedDocsRegistry;
 17use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
 18use node_runtime::NodeRuntime;
 19use parking_lot::Mutex;
 20use project::{Project, DEFAULT_COMPLETION_CONTEXT};
 21use release_channel::AppVersion;
 22use reqwest_client::ReqwestClient;
 23use serde_json::json;
 24use settings::{Settings as _, SettingsStore};
 25use snippet_provider::SnippetRegistry;
 26use std::{
 27    ffi::OsString,
 28    path::{Path, PathBuf},
 29    sync::Arc,
 30};
 31use theme::ThemeRegistry;
 32use ureq_client::UreqClient;
 33use util::test::temp_tree;
 34
 35#[cfg(test)]
 36#[ctor::ctor]
 37fn init_logger() {
 38    if std::env::var("RUST_LOG").is_ok() {
 39        env_logger::init();
 40    }
 41}
 42
 43#[gpui::test]
 44async fn test_extension_store(cx: &mut TestAppContext) {
 45    init_test(cx);
 46
 47    let fs = FakeFs::new(cx.executor());
 48    let http_client = FakeHttpClient::with_200_response();
 49
 50    fs.insert_tree(
 51        "/the-extension-dir",
 52        json!({
 53            "installed": {
 54                "zed-monokai": {
 55                    "extension.json": r#"{
 56                        "id": "zed-monokai",
 57                        "name": "Zed Monokai",
 58                        "version": "2.0.0",
 59                        "themes": {
 60                            "Monokai Dark": "themes/monokai.json",
 61                            "Monokai Light": "themes/monokai.json",
 62                            "Monokai Pro Dark": "themes/monokai-pro.json",
 63                            "Monokai Pro Light": "themes/monokai-pro.json"
 64                        }
 65                    }"#,
 66                    "themes": {
 67                        "monokai.json": r#"{
 68                            "name": "Monokai",
 69                            "author": "Someone",
 70                            "themes": [
 71                                {
 72                                    "name": "Monokai Dark",
 73                                    "appearance": "dark",
 74                                    "style": {}
 75                                },
 76                                {
 77                                    "name": "Monokai Light",
 78                                    "appearance": "light",
 79                                    "style": {}
 80                                }
 81                            ]
 82                        }"#,
 83                        "monokai-pro.json": r#"{
 84                            "name": "Monokai Pro",
 85                            "author": "Someone",
 86                            "themes": [
 87                                {
 88                                    "name": "Monokai Pro Dark",
 89                                    "appearance": "dark",
 90                                    "style": {}
 91                                },
 92                                {
 93                                    "name": "Monokai Pro Light",
 94                                    "appearance": "light",
 95                                    "style": {}
 96                                }
 97                            ]
 98                        }"#,
 99                    }
100                },
101                "zed-ruby": {
102                    "extension.json": r#"{
103                        "id": "zed-ruby",
104                        "name": "Zed Ruby",
105                        "version": "1.0.0",
106                        "grammars": {
107                            "ruby": "grammars/ruby.wasm",
108                            "embedded_template": "grammars/embedded_template.wasm"
109                        },
110                        "languages": {
111                            "ruby": "languages/ruby",
112                            "erb": "languages/erb"
113                        }
114                    }"#,
115                    "grammars": {
116                        "ruby.wasm": "",
117                        "embedded_template.wasm": "",
118                    },
119                    "languages": {
120                        "ruby": {
121                            "config.toml": r#"
122                                name = "Ruby"
123                                grammar = "ruby"
124                                path_suffixes = ["rb"]
125                            "#,
126                            "highlights.scm": "",
127                        },
128                        "erb": {
129                            "config.toml": r#"
130                                name = "ERB"
131                                grammar = "embedded_template"
132                                path_suffixes = ["erb"]
133                            "#,
134                            "highlights.scm": "",
135                        }
136                    },
137                }
138            }
139        }),
140    )
141    .await;
142
143    let mut expected_index = ExtensionIndex {
144        extensions: [
145            (
146                "zed-ruby".into(),
147                ExtensionIndexEntry {
148                    manifest: Arc::new(ExtensionManifest {
149                        id: "zed-ruby".into(),
150                        name: "Zed Ruby".into(),
151                        version: "1.0.0".into(),
152                        schema_version: SchemaVersion::ZERO,
153                        description: None,
154                        authors: Vec::new(),
155                        repository: None,
156                        themes: Default::default(),
157                        lib: Default::default(),
158                        languages: vec!["languages/erb".into(), "languages/ruby".into()],
159                        grammars: [
160                            ("embedded_template".into(), GrammarManifestEntry::default()),
161                            ("ruby".into(), GrammarManifestEntry::default()),
162                        ]
163                        .into_iter()
164                        .collect(),
165                        language_servers: BTreeMap::default(),
166                        slash_commands: BTreeMap::default(),
167                        indexed_docs_providers: BTreeMap::default(),
168                        snippets: None,
169                    }),
170                    dev: false,
171                },
172            ),
173            (
174                "zed-monokai".into(),
175                ExtensionIndexEntry {
176                    manifest: Arc::new(ExtensionManifest {
177                        id: "zed-monokai".into(),
178                        name: "Zed Monokai".into(),
179                        version: "2.0.0".into(),
180                        schema_version: SchemaVersion::ZERO,
181                        description: None,
182                        authors: vec![],
183                        repository: None,
184                        themes: vec![
185                            "themes/monokai-pro.json".into(),
186                            "themes/monokai.json".into(),
187                        ],
188                        lib: Default::default(),
189                        languages: Default::default(),
190                        grammars: BTreeMap::default(),
191                        language_servers: BTreeMap::default(),
192                        slash_commands: BTreeMap::default(),
193                        indexed_docs_providers: BTreeMap::default(),
194                        snippets: None,
195                    }),
196                    dev: false,
197                },
198            ),
199        ]
200        .into_iter()
201        .collect(),
202        languages: [
203            (
204                "ERB".into(),
205                ExtensionIndexLanguageEntry {
206                    extension: "zed-ruby".into(),
207                    path: "languages/erb".into(),
208                    grammar: Some("embedded_template".into()),
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                    matcher: LanguageMatcher {
222                        path_suffixes: vec!["rb".into()],
223                        first_line_pattern: None,
224                    },
225                },
226            ),
227        ]
228        .into_iter()
229        .collect(),
230        themes: [
231            (
232                "Monokai Dark".into(),
233                ExtensionIndexThemeEntry {
234                    extension: "zed-monokai".into(),
235                    path: "themes/monokai.json".into(),
236                },
237            ),
238            (
239                "Monokai Light".into(),
240                ExtensionIndexThemeEntry {
241                    extension: "zed-monokai".into(),
242                    path: "themes/monokai.json".into(),
243                },
244            ),
245            (
246                "Monokai Pro Dark".into(),
247                ExtensionIndexThemeEntry {
248                    extension: "zed-monokai".into(),
249                    path: "themes/monokai-pro.json".into(),
250                },
251            ),
252            (
253                "Monokai Pro Light".into(),
254                ExtensionIndexThemeEntry {
255                    extension: "zed-monokai".into(),
256                    path: "themes/monokai-pro.json".into(),
257                },
258            ),
259        ]
260        .into_iter()
261        .collect(),
262    };
263
264    let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
265    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
266    let slash_command_registry = SlashCommandRegistry::new();
267    let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
268    let snippet_registry = Arc::new(SnippetRegistry::new());
269    let node_runtime = NodeRuntime::unavailable();
270
271    let store = cx.new_model(|cx| {
272        ExtensionStore::new(
273            PathBuf::from("/the-extension-dir"),
274            None,
275            fs.clone(),
276            http_client.clone(),
277            http_client.clone(),
278            None,
279            node_runtime.clone(),
280            language_registry.clone(),
281            theme_registry.clone(),
282            slash_command_registry.clone(),
283            indexed_docs_registry.clone(),
284            snippet_registry.clone(),
285            cx,
286        )
287    });
288
289    cx.executor().advance_clock(super::RELOAD_DEBOUNCE_DURATION);
290    store.read_with(cx, |store, _| {
291        let index = &store.extension_index;
292        assert_eq!(index.extensions, expected_index.extensions);
293        assert_eq!(index.languages, expected_index.languages);
294        assert_eq!(index.themes, expected_index.themes);
295
296        assert_eq!(
297            language_registry.language_names(),
298            ["ERB", "Plain Text", "Ruby"]
299        );
300        assert_eq!(
301            theme_registry.list_names(false),
302            [
303                "Monokai Dark",
304                "Monokai Light",
305                "Monokai Pro Dark",
306                "Monokai Pro Light",
307                "One Dark",
308            ]
309        );
310    });
311
312    fs.insert_tree(
313        "/the-extension-dir/installed/zed-gruvbox",
314        json!({
315            "extension.json": r#"{
316                "id": "zed-gruvbox",
317                "name": "Zed Gruvbox",
318                "version": "1.0.0",
319                "themes": {
320                    "Gruvbox": "themes/gruvbox.json"
321                }
322            }"#,
323            "themes": {
324                "gruvbox.json": r#"{
325                    "name": "Gruvbox",
326                    "author": "Someone Else",
327                    "themes": [
328                        {
329                            "name": "Gruvbox",
330                            "appearance": "dark",
331                            "style": {}
332                        }
333                    ]
334                }"#,
335            }
336        }),
337    )
338    .await;
339
340    expected_index.extensions.insert(
341        "zed-gruvbox".into(),
342        ExtensionIndexEntry {
343            manifest: Arc::new(ExtensionManifest {
344                id: "zed-gruvbox".into(),
345                name: "Zed Gruvbox".into(),
346                version: "1.0.0".into(),
347                schema_version: SchemaVersion::ZERO,
348                description: None,
349                authors: vec![],
350                repository: None,
351                themes: vec!["themes/gruvbox.json".into()],
352                lib: Default::default(),
353                languages: Default::default(),
354                grammars: BTreeMap::default(),
355                language_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(false),
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            fs.clone(),
404            http_client.clone(),
405            http_client.clone(),
406            None,
407            node_runtime.clone(),
408            language_registry.clone(),
409            theme_registry.clone(),
410            slash_command_registry,
411            indexed_docs_registry,
412            snippet_registry,
413            cx,
414        )
415    });
416
417    cx.executor().run_until_parked();
418    store.read_with(cx, |store, _| {
419        assert_eq!(store.extension_index, expected_index);
420        assert_eq!(
421            language_registry.language_names(),
422            ["ERB", "Plain Text", "Ruby"]
423        );
424        assert_eq!(
425            language_registry.grammar_names(),
426            ["embedded_template".into(), "ruby".into()]
427        );
428        assert_eq!(
429            theme_registry.list_names(false),
430            [
431                "Gruvbox",
432                "Monokai Dark",
433                "Monokai Light",
434                "Monokai Pro Dark",
435                "Monokai Pro Light",
436                "One Dark",
437            ]
438        );
439
440        // The on-disk manifest limits the number of FS calls that need to be made
441        // on startup.
442        assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
443        assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
444    });
445
446    store.update(cx, |store, cx| {
447        store.uninstall_extension("zed-ruby".into(), cx)
448    });
449
450    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
451    expected_index.extensions.remove("zed-ruby");
452    expected_index.languages.remove("Ruby");
453    expected_index.languages.remove("ERB");
454
455    store.read_with(cx, |store, _| {
456        assert_eq!(store.extension_index, expected_index);
457        assert_eq!(language_registry.language_names(), ["Plain Text"]);
458        assert_eq!(language_registry.grammar_names(), []);
459    });
460}
461
462#[gpui::test]
463async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
464    init_test(cx);
465    cx.executor().allow_parking();
466
467    let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
468        .parent()
469        .unwrap()
470        .parent()
471        .unwrap();
472    let cache_dir = root_dir.join("target");
473    let test_extension_id = "test-extension";
474    let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
475
476    let fs = Arc::new(RealFs::default());
477    let extensions_dir = temp_tree(json!({
478        "installed": {},
479        "work": {}
480    }));
481    let project_dir = temp_tree(json!({
482        "test.gleam": ""
483    }));
484
485    let extensions_dir = extensions_dir.path().canonicalize().unwrap();
486    let project_dir = project_dir.path().canonicalize().unwrap();
487
488    let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
489
490    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
491    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
492    let slash_command_registry = SlashCommandRegistry::new();
493    let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
494    let snippet_registry = Arc::new(SnippetRegistry::new());
495    let node_runtime = NodeRuntime::unavailable();
496
497    let mut status_updates = language_registry.language_server_binary_statuses();
498
499    struct FakeLanguageServerVersion {
500        version: String,
501        binary_contents: String,
502        http_request_count: usize,
503    }
504
505    let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
506        version: "v1.2.3".into(),
507        binary_contents: "the-binary-contents".into(),
508        http_request_count: 0,
509    }));
510
511    let extension_client = FakeHttpClient::create({
512        let language_server_version = language_server_version.clone();
513        move |request| {
514            let language_server_version = language_server_version.clone();
515            async move {
516                let version = language_server_version.lock().version.clone();
517                let binary_contents = language_server_version.lock().binary_contents.clone();
518
519                let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
520                let asset_download_uri =
521                    format!("https://fake-download.example.com/gleam-{version}");
522
523                let uri = request.uri().to_string();
524                if uri == github_releases_uri {
525                    language_server_version.lock().http_request_count += 1;
526                    Ok(Response::new(
527                        json!([
528                            {
529                                "tag_name": version,
530                                "prerelease": false,
531                                "tarball_url": "",
532                                "zipball_url": "",
533                                "assets": [
534                                    {
535                                        "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
536                                        "browser_download_url": asset_download_uri
537                                    },
538                                    {
539                                        "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
540                                        "browser_download_url": asset_download_uri
541                                    },
542                                    {
543                                        "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
544                                        "browser_download_url": asset_download_uri
545                                    }
546                                ]
547                            }
548                        ])
549                        .to_string()
550                        .into(),
551                    ))
552                } else if uri == asset_download_uri {
553                    language_server_version.lock().http_request_count += 1;
554                    let mut bytes = Vec::<u8>::new();
555                    let mut archive = async_tar::Builder::new(&mut bytes);
556                    let mut header = async_tar::Header::new_gnu();
557                    header.set_size(binary_contents.len() as u64);
558                    archive
559                        .append_data(&mut header, "gleam", binary_contents.as_bytes())
560                        .await
561                        .unwrap();
562                    archive.into_inner().await.unwrap();
563                    let mut gzipped_bytes = Vec::new();
564                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
565                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
566                    Ok(Response::new(gzipped_bytes.into()))
567                } else {
568                    Ok(Response::builder().status(404).body("not found".into())?)
569                }
570            }
571        }
572    });
573    let user_agent = cx.update(|cx| {
574        format!(
575            "Zed/{} ({}; {})",
576            AppVersion::global(cx),
577            std::env::consts::OS,
578            std::env::consts::ARCH
579        )
580    });
581    let builder_client = Arc::new(UreqClient::new(None, user_agent, cx.executor().clone()));
582
583    let extension_store = cx.new_model(|cx| {
584        ExtensionStore::new(
585            extensions_dir.clone(),
586            Some(cache_dir),
587            fs.clone(),
588            extension_client.clone(),
589            builder_client,
590            None,
591            node_runtime,
592            language_registry.clone(),
593            theme_registry.clone(),
594            slash_command_registry,
595            indexed_docs_registry,
596            snippet_registry,
597            cx,
598        )
599    });
600
601    // Ensure that debounces fire.
602    let mut events = cx.events(&extension_store);
603    let executor = cx.executor();
604    let _task = cx.executor().spawn(async move {
605        while let Some(event) = events.next().await {
606            if let crate::Event::StartedReloading = event {
607                executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
608            }
609        }
610    });
611
612    extension_store.update(cx, |_, cx| {
613        cx.subscribe(&extension_store, |_, _, event, _| {
614            if matches!(event, Event::ExtensionFailedToLoad(_)) {
615                panic!("extension failed to load");
616            }
617        })
618        .detach();
619    });
620
621    extension_store
622        .update(cx, |store, cx| {
623            store.install_dev_extension(test_extension_dir.clone(), cx)
624        })
625        .await
626        .unwrap();
627
628    let mut fake_servers = language_registry.register_fake_language_server(
629        LanguageServerName("gleam".into()),
630        lsp::ServerCapabilities {
631            completion_provider: Some(Default::default()),
632            ..Default::default()
633        },
634        None,
635    );
636
637    let buffer = project
638        .update(cx, |project, cx| {
639            project.open_local_buffer(project_dir.join("test.gleam"), cx)
640        })
641        .await
642        .unwrap();
643
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([buffer.clone()], cx)
735    });
736
737    // The extension has cached the binary path, and does not attempt
738    // to reinstall it.
739    let fake_server = fake_servers.next().await.unwrap();
740    assert_eq!(fake_server.binary.path, expected_server_path);
741    assert_eq!(
742        fs.load(&expected_server_path).await.unwrap(),
743        expected_binary_contents
744    );
745    assert_eq!(language_server_version.lock().http_request_count, 0);
746
747    // Reload the extension, clearing its cache.
748    // Start a new instance of the language server.
749    extension_store
750        .update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
751        .await;
752
753    cx.executor().run_until_parked();
754    project.update(cx, |project, cx| {
755        project.restart_language_servers_for_buffers([buffer.clone()], cx)
756    });
757
758    // The extension re-fetches the latest version of the language server.
759    let fake_server = fake_servers.next().await.unwrap();
760    let new_expected_server_path =
761        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
762    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
763    assert_eq!(fake_server.binary.path, new_expected_server_path);
764    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
765    assert_eq!(
766        fs.load(&new_expected_server_path).await.unwrap(),
767        expected_binary_contents
768    );
769
770    // The old language server directory has been cleaned up.
771    assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
772}
773
774#[gpui::test]
775async fn test_wasi_adapter_download(cx: &mut TestAppContext) {
776    let client = Arc::new(UreqClient::new(
777        None,
778        "zed-test-wasi-adapter-download".to_string(),
779        cx.executor().clone(),
780    ));
781
782    let mut response = client
783        .get(WASI_ADAPTER_URL, AsyncBody::default(), true)
784        .await
785        .unwrap();
786
787    let mut content = Vec::new();
788    let mut body = BufReader::new(response.body_mut());
789    body.read_to_end(&mut content).await.unwrap();
790
791    assert!(wasmparser::Parser::is_core_wasm(&content));
792    assert_eq!(content.len(), 96801); // Determined by downloading this to my computer
793    wit_component::ComponentEncoder::default()
794        .adapter("wasi_snapshot_preview1", &content)
795        .unwrap();
796}
797
798#[tokio::test]
799async fn test_wasi_adapter_download_tokio() {
800    let client = Arc::new(ReqwestClient::new());
801
802    let mut response = client
803        .get(WASI_ADAPTER_URL, AsyncBody::default(), true)
804        .await
805        .unwrap();
806
807    let mut content = Vec::new();
808    let mut body = BufReader::new(response.body_mut());
809    body.read_to_end(&mut content).await.unwrap();
810
811    assert!(wasmparser::Parser::is_core_wasm(&content));
812    assert_eq!(content.len(), 96801); // Determined by downloading this to my computer
813    wit_component::ComponentEncoder::default()
814        .adapter("wasi_snapshot_preview1", &content)
815        .unwrap();
816}
817
818fn init_test(cx: &mut TestAppContext) {
819    cx.update(|cx| {
820        let store = SettingsStore::test(cx);
821        cx.set_global(store);
822        release_channel::init(SemanticVersion::default(), cx);
823        theme::init(theme::LoadThemes::JustBase, cx);
824        Project::init_settings(cx);
825        ExtensionSettings::register(cx);
826        language::init(cx);
827    });
828}