Skip to repository content1244 lines · 42.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:54:41.351Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
extension_store_test.rs
1use crate::{
2 Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
3 ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
4 RELOAD_DEBOUNCE_DURATION, SchemaVersion,
5};
6use async_compression::futures::bufread::GzipEncoder;
7use collections::{BTreeMap, HashSet};
8use extension::ExtensionHostProxy;
9use fs::{FakeFs, Fs, RealFs};
10use futures::{AsyncReadExt, FutureExt, StreamExt, io::BufReader};
11use gpui::{AppContext as _, BackgroundExecutor, TaskExt, TestAppContext};
12use http_client::{FakeHttpClient, Response};
13use language::{BinaryStatus, LanguageMatcher, LanguageName, LanguageRegistry};
14use language_extension::LspAccess;
15use lsp::LanguageServerName;
16use node_runtime::NodeRuntime;
17use parking_lot::Mutex;
18use project::{DEFAULT_COMPLETION_CONTEXT, Project};
19use release_channel::AppVersion;
20use reqwest_client::ReqwestClient;
21use serde_json::json;
22use settings::SettingsStore;
23use std::{
24 ffi::OsString,
25 path::{Path, PathBuf},
26 sync::Arc,
27};
28use theme::ThemeRegistry;
29use util::{rel_path::rel_path_buf, test::TempTree};
30
31#[cfg(test)]
32#[ctor::ctor(unsafe)]
33fn init_logger() {
34 zlog::init_test();
35}
36
37fn remote_sync_entry(id: &str, manifest_body: &str) -> ExtensionIndexEntry {
38 let manifest = format!(
39 r#"
40 id = "{id}"
41 name = "{id}"
42 version = "1.0.0"
43 schema_version = 0
44
45 {manifest_body}
46 "#
47 );
48
49 ExtensionIndexEntry {
50 manifest: Arc::new(toml::from_str(&manifest).unwrap()),
51 dev: false,
52 }
53}
54
55fn remote_sync_language_entry(extension: &str, path: &str) -> ExtensionIndexLanguageEntry {
56 ExtensionIndexLanguageEntry {
57 extension: extension.into(),
58 path: path.into(),
59 matcher: LanguageMatcher::default().into(),
60 hidden: false,
61 grammar: None,
62 }
63}
64
65fn remote_sync_extension_ids(index: &ExtensionIndex) -> Vec<String> {
66 let mut extensions = index
67 .extensions_to_sync_to_remote()
68 .into_entries()
69 .map(|(id, _)| id.to_string())
70 .collect::<Vec<_>>();
71
72 extensions.sort();
73
74 extensions
75}
76
77#[test]
78fn remote_sync_includes_language_dependencies() {
79 let index = ExtensionIndex {
80 extensions: [
81 (
82 "bar-language".into(),
83 remote_sync_entry("bar-language", r#"languages = ["languages/bar"]"#),
84 ),
85 (
86 "foo-lsp".into(),
87 remote_sync_entry(
88 "foo-lsp",
89 r#"
90 [language_servers.foo]
91 language = "Foo"
92 "#,
93 ),
94 ),
95 (
96 "foo-language".into(),
97 remote_sync_entry("foo-language", r#"languages = ["languages/foo"]"#),
98 ),
99 ]
100 .into_iter()
101 .collect(),
102 languages: [
103 (
104 "Bar".into(),
105 remote_sync_language_entry("bar-language", "languages/bar"),
106 ),
107 (
108 "Foo".into(),
109 remote_sync_language_entry("foo-language", "languages/foo"),
110 ),
111 ]
112 .into_iter()
113 .collect(),
114 themes: BTreeMap::default(),
115 icon_themes: BTreeMap::default(),
116 };
117
118 assert_eq!(
119 remote_sync_extension_ids(&index),
120 ["foo-language", "foo-lsp"]
121 );
122}
123
124#[test]
125fn remote_sync_keeps_shared_language_dependency_once() {
126 let index = ExtensionIndex {
127 extensions: [
128 (
129 "aaa-lsp".into(),
130 remote_sync_entry(
131 "aaa-lsp",
132 r#"
133 [language_servers.aaa]
134 language = "Foo"
135 "#,
136 ),
137 ),
138 (
139 "bbb-lsp".into(),
140 remote_sync_entry(
141 "bbb-lsp",
142 r#"
143 [language_servers.bbb]
144 language = "Foo"
145 "#,
146 ),
147 ),
148 (
149 "zzz-language".into(),
150 remote_sync_entry("zzz-language", r#"languages = ["languages/foo"]"#),
151 ),
152 ]
153 .into_iter()
154 .collect(),
155 languages: [(
156 "Foo".into(),
157 remote_sync_language_entry("zzz-language", "languages/foo"),
158 )]
159 .into_iter()
160 .collect(),
161 themes: BTreeMap::default(),
162 icon_themes: BTreeMap::default(),
163 };
164
165 assert_eq!(
166 remote_sync_extension_ids(&index),
167 ["aaa-lsp", "bbb-lsp", "zzz-language"]
168 );
169}
170
171#[test]
172fn remote_sync_keeps_remote_loadable_extensions_without_language_dependency() {
173 let index = ExtensionIndex {
174 extensions: [(
175 "foo".into(),
176 remote_sync_entry(
177 "foo",
178 r#"
179 [language_servers.foo]
180 language = "Foo"
181 "#,
182 ),
183 )]
184 .into_iter()
185 .collect(),
186 languages: BTreeMap::default(),
187 themes: BTreeMap::default(),
188 icon_themes: BTreeMap::default(),
189 };
190
191 assert_eq!(remote_sync_extension_ids(&index), ["foo"]);
192}
193
194#[test]
195fn remote_sync_keeps_debug_adapters() {
196 let index = ExtensionIndex {
197 extensions: [(
198 "foo".into(),
199 remote_sync_entry(
200 "foo",
201 r#"
202 [debug_adapters.foo]
203 "#,
204 ),
205 )]
206 .into_iter()
207 .collect(),
208 languages: BTreeMap::default(),
209 themes: BTreeMap::default(),
210 icon_themes: BTreeMap::default(),
211 };
212
213 assert_eq!(remote_sync_extension_ids(&index), ["foo"]);
214}
215
216#[gpui::test]
217async fn test_extension_store(cx: &mut TestAppContext) {
218 init_test(cx);
219
220 let fs = FakeFs::new(cx.executor());
221 let http_client = FakeHttpClient::with_200_response();
222
223 fs.insert_tree(
224 "/the-extension-dir",
225 json!({
226 "installed": {
227 "zed-monokai": {
228 "extension.json": r#"{
229 "id": "zed-monokai",
230 "name": "Zed Monokai",
231 "version": "2.0.0",
232 "themes": {
233 "Monokai Dark": "themes/monokai.json",
234 "Monokai Light": "themes/monokai.json",
235 "Monokai Pro Dark": "themes/monokai-pro.json",
236 "Monokai Pro Light": "themes/monokai-pro.json"
237 }
238 }"#,
239 "themes": {
240 "monokai.json": r#"{
241 "name": "Monokai",
242 "author": "Someone",
243 "themes": [
244 {
245 "name": "Monokai Dark",
246 "appearance": "dark",
247 "style": {}
248 },
249 {
250 "name": "Monokai Light",
251 "appearance": "light",
252 "style": {}
253 }
254 ]
255 }"#,
256 "monokai-pro.json": r#"{
257 "name": "Monokai Pro",
258 "author": "Someone",
259 "themes": [
260 {
261 "name": "Monokai Pro Dark",
262 "appearance": "dark",
263 "style": {}
264 },
265 {
266 "name": "Monokai Pro Light",
267 "appearance": "light",
268 "style": {}
269 }
270 ]
271 }"#,
272 }
273 },
274 "zed-ruby": {
275 "extension.json": r#"{
276 "id": "zed-ruby",
277 "name": "Zed Ruby",
278 "version": "1.0.0",
279 "grammars": {
280 "ruby": "grammars/ruby.wasm",
281 "embedded_template": "grammars/embedded_template.wasm"
282 },
283 "languages": {
284 "ruby": "languages/ruby",
285 "erb": "languages/erb"
286 }
287 }"#,
288 "grammars": {
289 "ruby.wasm": "",
290 "embedded_template.wasm": "",
291 },
292 "languages": {
293 "ruby": {
294 "config.toml": r#"
295 name = "Ruby"
296 grammar = "ruby"
297 path_suffixes = ["rb"]
298 "#,
299 "highlights.scm": "",
300 },
301 "erb": {
302 "config.toml": r#"
303 name = "ERB"
304 grammar = "embedded_template"
305 path_suffixes = ["erb"]
306 "#,
307 "highlights.scm": "",
308 }
309 },
310 }
311 }
312 }),
313 )
314 .await;
315
316 let mut expected_index = ExtensionIndex {
317 extensions: [
318 (
319 "zed-ruby".into(),
320 ExtensionIndexEntry {
321 manifest: Arc::new(ExtensionManifest {
322 id: "zed-ruby".into(),
323 name: "Zed Ruby".into(),
324 version: "1.0.0".into(),
325 schema_version: SchemaVersion::ZERO,
326 description: None,
327 authors: Vec::new(),
328 repository: None,
329 themes: Default::default(),
330 icon_themes: Vec::new(),
331 lib: Default::default(),
332 languages: vec![
333 rel_path_buf("languages/erb"),
334 rel_path_buf("languages/ruby"),
335 ],
336 grammars: [
337 ("embedded_template".into(), GrammarManifestEntry::default()),
338 ("ruby".into(), GrammarManifestEntry::default()),
339 ]
340 .into_iter()
341 .collect(),
342 language_servers: BTreeMap::default(),
343 context_servers: BTreeMap::default(),
344 slash_commands: BTreeMap::default(),
345 snippets: None,
346 capabilities: Vec::new(),
347 debug_adapters: Default::default(),
348 debug_locators: Default::default(),
349 language_model_providers: BTreeMap::default(),
350 }),
351 dev: false,
352 },
353 ),
354 (
355 "zed-monokai".into(),
356 ExtensionIndexEntry {
357 manifest: Arc::new(ExtensionManifest {
358 id: "zed-monokai".into(),
359 name: "Zed Monokai".into(),
360 version: "2.0.0".into(),
361 schema_version: SchemaVersion::ZERO,
362 description: None,
363 authors: vec![],
364 repository: None,
365 themes: vec![
366 rel_path_buf("themes/monokai-pro.json"),
367 rel_path_buf("themes/monokai.json"),
368 ],
369 icon_themes: Vec::new(),
370 lib: Default::default(),
371 languages: Default::default(),
372 grammars: BTreeMap::default(),
373 language_servers: BTreeMap::default(),
374 context_servers: BTreeMap::default(),
375 slash_commands: BTreeMap::default(),
376 snippets: None,
377 capabilities: Vec::new(),
378 debug_adapters: Default::default(),
379 debug_locators: Default::default(),
380 language_model_providers: BTreeMap::default(),
381 }),
382 dev: false,
383 },
384 ),
385 ]
386 .into_iter()
387 .collect(),
388 languages: [
389 (
390 "ERB".into(),
391 ExtensionIndexLanguageEntry {
392 extension: "zed-ruby".into(),
393 path: "languages/erb".into(),
394 grammar: Some("embedded_template".into()),
395 hidden: false,
396 matcher: (LanguageMatcher {
397 path_suffixes: vec!["erb".into()],
398 first_line_pattern: None,
399 ..LanguageMatcher::default()
400 })
401 .into(),
402 },
403 ),
404 (
405 "Ruby".into(),
406 ExtensionIndexLanguageEntry {
407 extension: "zed-ruby".into(),
408 path: "languages/ruby".into(),
409 grammar: Some("ruby".into()),
410 hidden: false,
411 matcher: (LanguageMatcher {
412 path_suffixes: vec!["rb".into()],
413 first_line_pattern: None,
414 ..LanguageMatcher::default()
415 })
416 .into(),
417 },
418 ),
419 ]
420 .into_iter()
421 .collect(),
422 themes: [
423 (
424 "Monokai Dark".into(),
425 ExtensionIndexThemeEntry {
426 extension: "zed-monokai".into(),
427 path: "themes/monokai.json".into(),
428 },
429 ),
430 (
431 "Monokai Light".into(),
432 ExtensionIndexThemeEntry {
433 extension: "zed-monokai".into(),
434 path: "themes/monokai.json".into(),
435 },
436 ),
437 (
438 "Monokai Pro Dark".into(),
439 ExtensionIndexThemeEntry {
440 extension: "zed-monokai".into(),
441 path: "themes/monokai-pro.json".into(),
442 },
443 ),
444 (
445 "Monokai Pro Light".into(),
446 ExtensionIndexThemeEntry {
447 extension: "zed-monokai".into(),
448 path: "themes/monokai-pro.json".into(),
449 },
450 ),
451 ]
452 .into_iter()
453 .collect(),
454 icon_themes: BTreeMap::default(),
455 };
456
457 let proxy = Arc::new(ExtensionHostProxy::new());
458 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
459 theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
460 let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
461 language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone());
462 let node_runtime = NodeRuntime::unavailable();
463
464 let store = cx.new(|cx| {
465 ExtensionStore::new(
466 PathBuf::from("/the-extension-dir"),
467 None,
468 proxy.clone(),
469 fs.clone(),
470 http_client.clone(),
471 http_client.clone(),
472 None,
473 node_runtime.clone(),
474 cx,
475 )
476 });
477
478 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
479 store.read_with(cx, |store, _| {
480 let index = &store.extension_index;
481 assert_eq!(index.extensions, expected_index.extensions);
482
483 for ((actual_key, actual_language), (expected_key, expected_language)) in
484 index.languages.iter().zip(expected_index.languages.iter())
485 {
486 assert_eq!(actual_key, expected_key);
487 assert_eq!(actual_language.grammar, expected_language.grammar);
488 assert_eq!(actual_language.matcher, expected_language.matcher);
489 assert_eq!(actual_language.hidden, expected_language.hidden);
490 }
491 assert_eq!(index.themes, expected_index.themes);
492
493 assert_eq!(
494 language_registry.language_names(),
495 [
496 LanguageName::new_static("ERB"),
497 LanguageName::new_static("Plain Text"),
498 LanguageName::new_static("Ruby"),
499 ]
500 );
501 assert_eq!(
502 theme_registry.list_names(),
503 [
504 "Monokai Dark",
505 "Monokai Light",
506 "Monokai Pro Dark",
507 "Monokai Pro Light",
508 "One Dark",
509 ]
510 );
511 });
512
513 fs.insert_tree(
514 "/the-extension-dir/installed/zed-gruvbox",
515 json!({
516 "extension.json": r#"{
517 "id": "zed-gruvbox",
518 "name": "Zed Gruvbox",
519 "version": "1.0.0",
520 "themes": {
521 "Gruvbox": "themes/gruvbox.json"
522 }
523 }"#,
524 "themes": {
525 "gruvbox.json": r#"{
526 "name": "Gruvbox",
527 "author": "Someone Else",
528 "themes": [
529 {
530 "name": "Gruvbox",
531 "appearance": "dark",
532 "style": {}
533 }
534 ]
535 }"#,
536 }
537 }),
538 )
539 .await;
540
541 expected_index.extensions.insert(
542 "zed-gruvbox".into(),
543 ExtensionIndexEntry {
544 manifest: Arc::new(ExtensionManifest {
545 id: "zed-gruvbox".into(),
546 name: "Zed Gruvbox".into(),
547 version: "1.0.0".into(),
548 schema_version: SchemaVersion::ZERO,
549 description: None,
550 authors: vec![],
551 repository: None,
552 themes: vec![rel_path_buf("themes/gruvbox.json")],
553 icon_themes: Vec::new(),
554 lib: Default::default(),
555 languages: Default::default(),
556 grammars: BTreeMap::default(),
557 language_servers: BTreeMap::default(),
558 context_servers: BTreeMap::default(),
559 slash_commands: BTreeMap::default(),
560 snippets: None,
561 capabilities: Vec::new(),
562 debug_adapters: Default::default(),
563 debug_locators: Default::default(),
564 language_model_providers: BTreeMap::default(),
565 }),
566 dev: false,
567 },
568 );
569 expected_index.themes.insert(
570 "Gruvbox".into(),
571 ExtensionIndexThemeEntry {
572 extension: "zed-gruvbox".into(),
573 path: "themes/gruvbox.json".into(),
574 },
575 );
576
577 #[allow(clippy::let_underscore_future)]
578 let _ = store.update(cx, |store, cx| store.reload(None, cx));
579
580 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
581 store.read_with(cx, |store, _| {
582 let index = &store.extension_index;
583
584 for ((actual_key, actual_language), (expected_key, expected_language)) in
585 index.languages.iter().zip(expected_index.languages.iter())
586 {
587 assert_eq!(actual_key, expected_key);
588 assert_eq!(actual_language.grammar, expected_language.grammar);
589 assert_eq!(actual_language.matcher, expected_language.matcher);
590 assert_eq!(actual_language.hidden, expected_language.hidden);
591 }
592
593 assert_eq!(index.extensions, expected_index.extensions);
594 assert_eq!(index.themes, expected_index.themes);
595
596 assert_eq!(
597 theme_registry.list_names(),
598 [
599 "Gruvbox",
600 "Monokai Dark",
601 "Monokai Light",
602 "Monokai Pro Dark",
603 "Monokai Pro Light",
604 "One Dark",
605 ]
606 );
607 });
608
609 let prev_fs_metadata_call_count = fs.metadata_call_count();
610 let prev_fs_read_dir_call_count = fs.read_dir_call_count();
611
612 // Create new extension store, as if Zed were restarting.
613 drop(store);
614 let store = cx.new(|cx| {
615 ExtensionStore::new(
616 PathBuf::from("/the-extension-dir"),
617 None,
618 proxy,
619 fs.clone(),
620 http_client.clone(),
621 http_client.clone(),
622 None,
623 node_runtime.clone(),
624 cx,
625 )
626 });
627
628 cx.executor().run_until_parked();
629 store.read_with(cx, |store, _| {
630 assert_eq!(store.extension_index.extensions, expected_index.extensions);
631 assert_eq!(store.extension_index.themes, expected_index.themes);
632 assert_eq!(
633 store.extension_index.icon_themes,
634 expected_index.icon_themes
635 );
636
637 for ((actual_key, actual_language), (expected_key, expected_language)) in store
638 .extension_index
639 .languages
640 .iter()
641 .zip(expected_index.languages.iter())
642 {
643 assert_eq!(actual_key, expected_key);
644 assert_eq!(actual_language.grammar, expected_language.grammar);
645 assert_eq!(actual_language.matcher, expected_language.matcher);
646 assert_eq!(actual_language.hidden, expected_language.hidden);
647 }
648
649 assert_eq!(
650 language_registry.language_names(),
651 [
652 LanguageName::new_static("ERB"),
653 LanguageName::new_static("Plain Text"),
654 LanguageName::new_static("Ruby"),
655 ]
656 );
657 assert_eq!(
658 language_registry.grammar_names(),
659 ["embedded_template".into(), "ruby".into()]
660 );
661 assert_eq!(
662 theme_registry.list_names(),
663 [
664 "Gruvbox",
665 "Monokai Dark",
666 "Monokai Light",
667 "Monokai Pro Dark",
668 "Monokai Pro Light",
669 "One Dark",
670 ]
671 );
672
673 // The on-disk manifest limits the number of FS calls that need to be made
674 // on startup.
675 assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
676 assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
677 });
678
679 store.update(cx, |store, cx| {
680 store
681 .uninstall_extension("zed-ruby".into(), cx)
682 .detach_and_log_err(cx);
683 });
684
685 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
686 expected_index.extensions.remove("zed-ruby");
687 expected_index.languages.remove("Ruby");
688 expected_index.languages.remove("ERB");
689
690 store.read_with(cx, |store, _| {
691 assert_eq!(store.extension_index.extensions, expected_index.extensions);
692 assert_eq!(store.extension_index.themes, expected_index.themes);
693 assert_eq!(
694 store.extension_index.icon_themes,
695 expected_index.icon_themes
696 );
697
698 for ((actual_key, actual_language), (expected_key, expected_language)) in store
699 .extension_index
700 .languages
701 .iter()
702 .zip(expected_index.languages.iter())
703 {
704 assert_eq!(actual_key, expected_key);
705 assert_eq!(actual_language.grammar, expected_language.grammar);
706 assert_eq!(actual_language.matcher, expected_language.matcher);
707 assert_eq!(actual_language.hidden, expected_language.hidden);
708 }
709
710 assert_eq!(
711 language_registry.language_names(),
712 [LanguageName::new_static("Plain Text")]
713 );
714 assert_eq!(language_registry.grammar_names(), []);
715 });
716}
717
718#[gpui::test]
719async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
720 init_test(cx);
721 cx.executor().allow_parking();
722
723 let executor = cx.executor();
724 async fn await_or_timeout<T>(
725 executor: &BackgroundExecutor,
726 what: &'static str,
727 seconds: u64,
728 future: impl std::future::Future<Output = T>,
729 ) -> T {
730 let timeout = executor.timer(std::time::Duration::from_secs(seconds));
731
732 futures::select! {
733 output = future.fuse() => output,
734 _ = futures::FutureExt::fuse(timeout) => panic!(
735 "[test_extension_store_with_test_extension] timed out after {seconds}s while {what}"
736 )
737 }
738 }
739
740 let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
741 .parent()
742 .unwrap()
743 .parent()
744 .unwrap();
745 let cache_dir = root_dir.join("target");
746 let test_extension_id = "test-extension";
747 let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
748
749 let fs = Arc::new(RealFs::new(None, cx.executor()));
750 let extensions_tree = TempTree::new(json!({
751 "installed": {},
752 "work": {}
753 }));
754 let project_dir = TempTree::new(json!({
755 "test.gleam": ""
756 }));
757
758 let extensions_dir = extensions_tree.path().canonicalize().unwrap();
759 let project_dir = project_dir.path().canonicalize().unwrap();
760
761 let project = await_or_timeout(
762 &executor,
763 "awaiting Project::test",
764 5,
765 Project::test(fs.clone(), [project_dir.as_path()], cx),
766 )
767 .await;
768
769 let proxy = Arc::new(ExtensionHostProxy::new());
770 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
771 theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
772 let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
773 language_extension::init(
774 LspAccess::ViaLspStore(
775 project
776 .update(cx, |project, _| project.lsp_store())
777 .downgrade(),
778 ),
779 proxy.clone(),
780 language_registry.clone(),
781 );
782 let node_runtime = NodeRuntime::unavailable();
783
784 let mut status_updates = language_registry.language_server_binary_statuses();
785
786 struct FakeLanguageServerVersion {
787 version: String,
788 binary_contents: String,
789 http_request_count: usize,
790 }
791
792 let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
793 version: "v1.2.3".into(),
794 binary_contents: "the-binary-contents".into(),
795 http_request_count: 0,
796 }));
797
798 let extension_client = FakeHttpClient::create({
799 let language_server_version = language_server_version.clone();
800 move |request| {
801 let language_server_version = language_server_version.clone();
802 async move {
803 let version = language_server_version.lock().version.clone();
804 let binary_contents = language_server_version.lock().binary_contents.clone();
805
806 let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
807 let asset_download_uri =
808 format!("https://fake-download.example.com/gleam-{version}");
809
810 let uri = request.uri().to_string();
811 if uri == github_releases_uri {
812 language_server_version.lock().http_request_count += 1;
813 Ok(Response::new(
814 json!([
815 {
816 "tag_name": version,
817 "prerelease": false,
818 "tarball_url": "",
819 "zipball_url": "",
820 "assets": [
821 {
822 "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
823 "browser_download_url": asset_download_uri
824 },
825 {
826 "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
827 "browser_download_url": asset_download_uri
828 },
829 {
830 "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
831 "browser_download_url": asset_download_uri
832 },
833 {
834 "name": format!("gleam-{version}-x86_64-pc-windows-msvc.tar.gz"),
835 "browser_download_url": asset_download_uri
836 }
837 ]
838 }
839 ])
840 .to_string()
841 .into(),
842 ))
843 } else if uri == asset_download_uri {
844 language_server_version.lock().http_request_count += 1;
845 let mut bytes = Vec::<u8>::new();
846 let mut archive = async_tar::Builder::new(&mut bytes);
847 let mut header = async_tar::Header::new_gnu();
848 header.set_size(binary_contents.len() as u64);
849 archive
850 .append_data(&mut header, "gleam", binary_contents.as_bytes())
851 .await
852 .unwrap();
853 archive.into_inner().await.unwrap();
854 let mut gzipped_bytes = Vec::new();
855 let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
856 encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
857 Ok(Response::new(gzipped_bytes.into()))
858 } else {
859 Ok(Response::builder().status(404).body("not found".into())?)
860 }
861 }
862 }
863 });
864 let user_agent = cx.update(|cx| {
865 format!(
866 "Zed/{} ({}; {})",
867 AppVersion::global(cx),
868 std::env::consts::OS,
869 std::env::consts::ARCH
870 )
871 });
872 let builder_client =
873 Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
874
875 let extension_store = cx.new(|cx| {
876 ExtensionStore::new(
877 extensions_dir.clone(),
878 Some(cache_dir),
879 proxy,
880 fs.clone(),
881 extension_client.clone(),
882 builder_client,
883 None,
884 node_runtime,
885 cx,
886 )
887 });
888
889 // Ensure that debounces fire.
890 let mut events = cx.events(&extension_store);
891 let executor = cx.executor();
892 let _task = cx.executor().spawn(async move {
893 while let Some(event) = events.next().await {
894 if let Event::StartedReloading = event {
895 executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
896 }
897 }
898 });
899
900 extension_store.update(cx, |_, cx| {
901 cx.subscribe(&extension_store, |_, _, event, _| {
902 if matches!(event, Event::ExtensionFailedToLoad(_)) {
903 panic!("extension failed to load");
904 }
905 })
906 .detach();
907 });
908
909 let mut extension_events = cx.events(&cx.update(|cx| {
910 extension::ExtensionEvents::try_global(cx)
911 .expect("ExtensionEvents should be initialized in tests")
912 }));
913
914 let executor = cx.executor();
915 await_or_timeout(
916 &executor,
917 "awaiting install_dev_extension",
918 60,
919 extension_store.update(cx, |store, cx| {
920 store.install_dev_extension(test_extension_dir.clone(), cx)
921 }),
922 )
923 .await
924 .unwrap();
925
926 await_or_timeout(
927 &executor,
928 "awaiting ExtensionsInstalledChanged",
929 10,
930 async {
931 while let Some(event) = extension_events.next().await {
932 if matches!(event, extension::Event::ExtensionsInstalledChanged) {
933 return;
934 }
935 }
936
937 panic!(
938 "[test_extension_store_with_test_extension] extension event stream ended before ExtensionsInstalledChanged"
939 );
940 },
941 )
942 .await;
943
944 let mut fake_servers = language_registry.register_fake_lsp_server(
945 LanguageServerName("gleam".into()),
946 lsp::ServerCapabilities {
947 completion_provider: Some(Default::default()),
948 ..Default::default()
949 },
950 None,
951 );
952 cx.executor().run_until_parked();
953
954 let mut project_events = cx.events(&project);
955 let buffer_path = project_dir.join("test.gleam");
956 let (buffer, _handle) = await_or_timeout(
957 &executor,
958 "awaiting open_local_buffer_with_lsp",
959 5,
960 project.update(cx, |project, cx| {
961 project.open_local_buffer_with_lsp(buffer_path.clone(), cx)
962 }),
963 )
964 .await
965 .unwrap();
966 cx.executor().run_until_parked();
967
968 let buffer_remote_id = buffer.read_with(cx, |buffer, _cx| buffer.remote_id());
969
970 let fake_server = await_or_timeout(
971 &executor,
972 "awaiting first fake server spawn",
973 10,
974 fake_servers.next(),
975 )
976 .await
977 .unwrap();
978
979 let work_dir = extensions_dir.join(format!("work/{test_extension_id}"));
980 let expected_server_path = work_dir.join("gleam-v1.2.3/gleam");
981 let expected_binary_contents = language_server_version.lock().binary_contents.clone();
982
983 // check that IO operations in extension work correctly
984 assert!(work_dir.join("dir-created-with-rel-path").exists());
985 assert!(work_dir.join("dir-created-with-abs-path").exists());
986 assert!(work_dir.join("file-created-with-abs-path").exists());
987 assert!(work_dir.join("file-created-with-rel-path").exists());
988
989 assert_eq!(fake_server.binary.path, expected_server_path);
990 assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
991 assert_eq!(
992 await_or_timeout(
993 &executor,
994 "awaiting fs.load(expected_server_path)",
995 5,
996 fs.load(&expected_server_path)
997 )
998 .await
999 .unwrap(),
1000 expected_binary_contents
1001 );
1002 assert_eq!(language_server_version.lock().http_request_count, 2);
1003 assert_eq!(
1004 [
1005 await_or_timeout(
1006 &executor,
1007 "awaiting status_updates #1",
1008 5,
1009 status_updates.next()
1010 )
1011 .await
1012 .unwrap(),
1013 await_or_timeout(
1014 &executor,
1015 "awaiting status_updates #2",
1016 5,
1017 status_updates.next()
1018 )
1019 .await
1020 .unwrap(),
1021 await_or_timeout(
1022 &executor,
1023 "awaiting status_updates #3",
1024 5,
1025 status_updates.next()
1026 )
1027 .await
1028 .unwrap(),
1029 await_or_timeout(
1030 &executor,
1031 "awaiting status_updates #4",
1032 5,
1033 status_updates.next()
1034 )
1035 .await
1036 .unwrap(),
1037 ],
1038 [
1039 (
1040 LanguageServerName::new_static("gleam"),
1041 BinaryStatus::Starting
1042 ),
1043 (
1044 LanguageServerName::new_static("gleam"),
1045 BinaryStatus::CheckingForUpdate
1046 ),
1047 (
1048 LanguageServerName::new_static("gleam"),
1049 BinaryStatus::Downloading
1050 ),
1051 (LanguageServerName::new_static("gleam"), BinaryStatus::None)
1052 ]
1053 );
1054
1055 // The extension creates custom labels for completion items.
1056 fake_server.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
1057 Ok(Some(lsp::CompletionResponse::Array(vec![
1058 lsp::CompletionItem {
1059 label: "foo".into(),
1060 kind: Some(lsp::CompletionItemKind::FUNCTION),
1061 detail: Some("fn() -> Result(Nil, Error)".into()),
1062 ..Default::default()
1063 },
1064 lsp::CompletionItem {
1065 label: "bar.baz".into(),
1066 kind: Some(lsp::CompletionItemKind::FUNCTION),
1067 detail: Some("fn(List(a)) -> a".into()),
1068 ..Default::default()
1069 },
1070 lsp::CompletionItem {
1071 label: "Quux".into(),
1072 kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
1073 detail: Some("fn(String) -> T".into()),
1074 ..Default::default()
1075 },
1076 lsp::CompletionItem {
1077 label: "my_string".into(),
1078 kind: Some(lsp::CompletionItemKind::CONSTANT),
1079 detail: Some("String".into()),
1080 ..Default::default()
1081 },
1082 ])))
1083 });
1084
1085 // `register_fake_lsp_server` can yield a server instance before the client has fully registered
1086 // the buffer with the project LSP plumbing. Wait for the project to observe that registration
1087 // before issuing requests like completion.
1088 await_or_timeout(
1089 &executor,
1090 "awaiting LanguageServerBufferRegistered",
1091 5,
1092 async {
1093 while let Some(event) = project_events.next().await {
1094 if let project::Event::LanguageServerBufferRegistered { buffer_id, .. } = event {
1095 if buffer_id == buffer_remote_id {
1096 return;
1097 }
1098 }
1099 }
1100
1101 panic!(
1102 "[test_extension_store_with_test_extension] project event stream ended before buffer registration for {}",
1103 buffer_path.display()
1104 );
1105 },
1106 )
1107 .await;
1108
1109 let completion_labels = await_or_timeout(
1110 &executor,
1111 "awaiting completions",
1112 5,
1113 project.update(cx, |project, cx| {
1114 project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
1115 }),
1116 )
1117 .await
1118 .unwrap()
1119 .into_iter()
1120 .flat_map(|response| response.completions)
1121 .map(|c| c.label.text)
1122 .collect::<Vec<_>>();
1123 assert_eq!(
1124 completion_labels,
1125 [
1126 "foo: fn() -> Result(Nil, Error)".to_string(),
1127 "bar.baz: fn(List(a)) -> a".to_string(),
1128 "Quux: fn(String) -> T".to_string(),
1129 "my_string: String".to_string(),
1130 ]
1131 );
1132
1133 // Simulate a new version of the language server being released
1134 language_server_version.lock().version = "v2.0.0".into();
1135 language_server_version.lock().binary_contents = "the-new-binary-contents".into();
1136 language_server_version.lock().http_request_count = 0;
1137
1138 // Start a new instance of the language server.
1139 project.update(cx, |project, cx| {
1140 project.restart_language_servers_for_buffers(
1141 vec![buffer.clone()],
1142 HashSet::default(),
1143 true,
1144 cx,
1145 )
1146 });
1147 cx.executor().run_until_parked();
1148
1149 // The extension has cached the binary path, and does not attempt
1150 // to reinstall it.
1151 let fake_server = await_or_timeout(
1152 &executor,
1153 "awaiting second fake server spawn",
1154 5,
1155 fake_servers.next(),
1156 )
1157 .await
1158 .unwrap();
1159 assert_eq!(fake_server.binary.path, expected_server_path);
1160 assert_eq!(
1161 await_or_timeout(
1162 &executor,
1163 "awaiting fs.load(expected_server_path) after restart",
1164 5,
1165 fs.load(&expected_server_path)
1166 )
1167 .await
1168 .unwrap(),
1169 expected_binary_contents
1170 );
1171 assert_eq!(language_server_version.lock().http_request_count, 0);
1172
1173 // Reload the extension, clearing its cache.
1174 // Start a new instance of the language server.
1175 await_or_timeout(
1176 &executor,
1177 "awaiting extension_store.reload(test-extension)",
1178 5,
1179 extension_store.update(cx, |store, cx| {
1180 store.reload(Some("test-extension".into()), cx)
1181 }),
1182 )
1183 .await;
1184 cx.executor().run_until_parked();
1185 project.update(cx, |project, cx| {
1186 project.restart_language_servers_for_buffers(
1187 vec![buffer.clone()],
1188 HashSet::default(),
1189 true,
1190 cx,
1191 )
1192 });
1193
1194 // The extension re-fetches the latest version of the language server.
1195 let fake_server = await_or_timeout(
1196 &executor,
1197 "awaiting third fake server spawn",
1198 5,
1199 fake_servers.next(),
1200 )
1201 .await
1202 .unwrap();
1203 let new_expected_server_path =
1204 extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
1205 let expected_binary_contents = language_server_version.lock().binary_contents.clone();
1206 assert_eq!(fake_server.binary.path, new_expected_server_path);
1207 assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
1208 assert_eq!(
1209 await_or_timeout(
1210 &executor,
1211 "awaiting fs.load(new_expected_server_path)",
1212 5,
1213 fs.load(&new_expected_server_path)
1214 )
1215 .await
1216 .unwrap(),
1217 expected_binary_contents
1218 );
1219
1220 // The old language server directory has been cleaned up.
1221 assert!(
1222 await_or_timeout(
1223 &executor,
1224 "awaiting fs.metadata(expected_server_path)",
1225 5,
1226 fs.metadata(&expected_server_path)
1227 )
1228 .await
1229 .unwrap()
1230 .is_none()
1231 );
1232}
1233
1234fn init_test(cx: &mut TestAppContext) {
1235 cx.update(|cx| {
1236 let store = SettingsStore::test(cx);
1237 cx.set_global(store);
1238 release_channel::init(semver::Version::new(0, 0, 0), cx);
1239 extension::init(cx);
1240 theme_settings::init(theme::LoadThemes::JustBase, cx);
1241 gpui_tokio::init(cx);
1242 });
1243}
1244