1use dashmap::DashMap;
2use deps_core::HttpCache;
3use deps_core::lockfile::LockFileCache;
4use deps_core::{EcosystemRegistry, ParseResult};
5use std::collections::HashMap;
6
7#[cfg(feature = "cargo")]
8use deps_cargo::{CargoVersion, ParsedDependency};
9#[cfg(feature = "go")]
10use deps_go::{GoDependency, GoVersion};
11#[cfg(feature = "npm")]
12use deps_npm::{NpmDependency, NpmVersion};
13#[cfg(feature = "pypi")]
14use deps_pypi::{PypiDependency, PypiVersion};
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17use tokio::task::JoinHandle;
18use tower_lsp_server::ls_types::Uri;
19
20#[derive(Debug, Clone)]
25#[non_exhaustive]
26pub enum UnifiedDependency {
27 #[cfg(feature = "cargo")]
28 Cargo(ParsedDependency),
29 #[cfg(feature = "npm")]
30 Npm(NpmDependency),
31 #[cfg(feature = "pypi")]
32 Pypi(PypiDependency),
33 #[cfg(feature = "go")]
34 Go(GoDependency),
35}
36
37impl UnifiedDependency {
38 #[allow(unreachable_patterns)]
40 pub fn name(&self) -> &str {
41 match self {
42 #[cfg(feature = "cargo")]
43 Self::Cargo(dep) => &dep.name,
44 #[cfg(feature = "npm")]
45 Self::Npm(dep) => &dep.name,
46 #[cfg(feature = "pypi")]
47 Self::Pypi(dep) => &dep.name,
48 #[cfg(feature = "go")]
49 Self::Go(dep) => &dep.module_path,
50 _ => unreachable!("no ecosystem features enabled"),
51 }
52 }
53
54 #[allow(unreachable_patterns)]
56 pub fn name_range(&self) -> tower_lsp_server::ls_types::Range {
57 match self {
58 #[cfg(feature = "cargo")]
59 Self::Cargo(dep) => dep.name_range,
60 #[cfg(feature = "npm")]
61 Self::Npm(dep) => dep.name_range,
62 #[cfg(feature = "pypi")]
63 Self::Pypi(dep) => dep.name_range,
64 #[cfg(feature = "go")]
65 Self::Go(dep) => dep.module_path_range,
66 _ => unreachable!("no ecosystem features enabled"),
67 }
68 }
69
70 #[allow(unreachable_patterns)]
72 pub fn version_req(&self) -> Option<&str> {
73 match self {
74 #[cfg(feature = "cargo")]
75 Self::Cargo(dep) => dep.version_req.as_deref(),
76 #[cfg(feature = "npm")]
77 Self::Npm(dep) => dep.version_req.as_deref(),
78 #[cfg(feature = "pypi")]
79 Self::Pypi(dep) => dep.version_req.as_deref(),
80 #[cfg(feature = "go")]
81 Self::Go(dep) => dep.version.as_deref(),
82 _ => unreachable!("no ecosystem features enabled"),
83 }
84 }
85
86 #[allow(unreachable_patterns)]
88 pub fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
89 match self {
90 #[cfg(feature = "cargo")]
91 Self::Cargo(dep) => dep.version_range,
92 #[cfg(feature = "npm")]
93 Self::Npm(dep) => dep.version_range,
94 #[cfg(feature = "pypi")]
95 Self::Pypi(dep) => dep.version_range,
96 #[cfg(feature = "go")]
97 Self::Go(dep) => dep.version_range,
98 _ => unreachable!("no ecosystem features enabled"),
99 }
100 }
101
102 #[allow(unreachable_patterns)]
104 pub fn is_registry(&self) -> bool {
105 use deps_core::Dependency;
106 match self {
107 #[cfg(feature = "cargo")]
108 Self::Cargo(dep) => dep.source().is_registry(),
109 #[cfg(feature = "npm")]
110 Self::Npm(dep) => dep.source().is_registry(),
111 #[cfg(feature = "pypi")]
112 Self::Pypi(dep) => dep.source().is_registry(),
113 #[cfg(feature = "go")]
114 Self::Go(dep) => dep.source().is_registry(),
115 _ => unreachable!("no ecosystem features enabled"),
116 }
117 }
118}
119
120#[derive(Debug, Clone)]
124#[non_exhaustive]
125pub enum UnifiedVersion {
126 #[cfg(feature = "cargo")]
127 Cargo(CargoVersion),
128 #[cfg(feature = "npm")]
129 Npm(NpmVersion),
130 #[cfg(feature = "pypi")]
131 Pypi(PypiVersion),
132 #[cfg(feature = "go")]
133 Go(GoVersion),
134}
135
136impl UnifiedVersion {
137 #[allow(unreachable_patterns)]
139 pub fn version_string(&self) -> &str {
140 match self {
141 #[cfg(feature = "cargo")]
142 Self::Cargo(v) => &v.num,
143 #[cfg(feature = "npm")]
144 Self::Npm(v) => &v.version,
145 #[cfg(feature = "pypi")]
146 Self::Pypi(v) => &v.version,
147 #[cfg(feature = "go")]
148 Self::Go(v) => &v.version,
149 _ => unreachable!("no ecosystem features enabled"),
150 }
151 }
152
153 #[allow(unreachable_patterns)]
155 pub fn is_yanked(&self) -> bool {
156 match self {
157 #[cfg(feature = "cargo")]
158 Self::Cargo(v) => v.yanked,
159 #[cfg(feature = "npm")]
160 Self::Npm(v) => v.deprecated,
161 #[cfg(feature = "pypi")]
162 Self::Pypi(v) => v.yanked,
163 #[cfg(feature = "go")]
164 Self::Go(v) => v.retracted,
165 _ => unreachable!("no ecosystem features enabled"),
166 }
167 }
168}
169
170pub use deps_core::LoadingState;
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197#[non_exhaustive]
198pub enum Ecosystem {
199 Cargo,
201 Npm,
203 Pypi,
205 Go,
207}
208
209impl Ecosystem {
210 pub fn from_filename(filename: &str) -> Option<Self> {
215 match filename {
216 "Cargo.toml" => Some(Self::Cargo),
217 "package.json" => Some(Self::Npm),
218 "pyproject.toml" => Some(Self::Pypi),
219 "go.mod" => Some(Self::Go),
220 _ => None,
221 }
222 }
223
224 pub fn from_uri(uri: &Uri) -> Option<Self> {
228 let path = uri.path();
229 let filename = path.as_str().split('/').next_back()?;
230 Self::from_filename(filename)
231 }
232}
233
234pub struct DocumentState {
272 pub ecosystem: Ecosystem,
274 pub ecosystem_id: &'static str,
276 pub content: String,
278 pub dependencies: Vec<UnifiedDependency>,
280 #[allow(dead_code)]
283 parse_result: Option<Box<dyn ParseResult>>,
284 pub versions: HashMap<String, UnifiedVersion>,
286 pub cached_versions: HashMap<String, String>,
288 pub resolved_versions: HashMap<String, String>,
290 pub parsed_at: Instant,
292 pub loading_state: LoadingState,
294 pub loading_started_at: Option<Instant>,
296}
297
298impl Clone for DocumentState {
299 fn clone(&self) -> Self {
300 Self {
301 ecosystem: self.ecosystem,
302 ecosystem_id: self.ecosystem_id,
303 content: self.content.clone(),
304 dependencies: self.dependencies.clone(),
305 parse_result: None, versions: self.versions.clone(),
307 cached_versions: self.cached_versions.clone(),
308 resolved_versions: self.resolved_versions.clone(),
309 parsed_at: self.parsed_at,
310 loading_state: self.loading_state,
311 loading_started_at: self.loading_started_at,
313 }
314 }
315}
316
317#[derive(Debug)]
338pub struct ColdStartLimiter {
339 last_attempts: DashMap<Uri, Instant>,
341 min_interval: Duration,
343}
344
345impl ColdStartLimiter {
346 pub fn new(min_interval: Duration) -> Self {
348 Self {
349 last_attempts: DashMap::new(),
350 min_interval,
351 }
352 }
353
354 pub fn allow_cold_start(&self, uri: &Uri) -> bool {
358 let now = Instant::now();
359
360 if let Some(mut entry) = self.last_attempts.get_mut(uri) {
362 let elapsed = now.duration_since(*entry);
363 if elapsed < self.min_interval {
364 let retry_after = self.min_interval.checked_sub(elapsed).unwrap();
365 tracing::warn!(
366 "Cold start rate limited for {:?} (retry after {:?})",
367 uri,
368 retry_after
369 );
370 return false;
371 }
372 *entry = now;
373 } else {
374 self.last_attempts.insert(uri.clone(), now);
375 }
376
377 true
378 }
379
380 pub fn cleanup_old_entries(&self, max_age: Duration) {
385 let now = Instant::now();
386 self.last_attempts
387 .retain(|_, instant| now.duration_since(*instant) < max_age);
388 }
389
390 #[cfg(test)]
392 pub fn tracked_count(&self) -> usize {
393 self.last_attempts.len()
394 }
395}
396
397impl std::fmt::Debug for DocumentState {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 f.debug_struct("DocumentState")
400 .field("ecosystem", &self.ecosystem)
401 .field("ecosystem_id", &self.ecosystem_id)
402 .field("content_len", &self.content.len())
403 .field("dependencies_count", &self.dependencies.len())
404 .field("has_parse_result", &self.parse_result.is_some())
405 .field("versions_count", &self.versions.len())
406 .field("cached_versions_count", &self.cached_versions.len())
407 .field("resolved_versions_count", &self.resolved_versions.len())
408 .field("parsed_at", &self.parsed_at)
409 .field("loading_state", &self.loading_state)
410 .field("loading_started_at", &self.loading_started_at)
411 .finish()
412 }
413}
414
415impl DocumentState {
416 pub fn new(
421 ecosystem: Ecosystem,
422 content: String,
423 dependencies: Vec<UnifiedDependency>,
424 ) -> Self {
425 let ecosystem_id = match ecosystem {
426 Ecosystem::Cargo => "cargo",
427 Ecosystem::Npm => "npm",
428 Ecosystem::Pypi => "pypi",
429 Ecosystem::Go => "go",
430 };
431
432 Self {
433 ecosystem,
434 ecosystem_id,
435 content,
436 dependencies,
437 parse_result: None,
438 versions: HashMap::new(),
439 cached_versions: HashMap::new(),
440 resolved_versions: HashMap::new(),
441 parsed_at: Instant::now(),
442 loading_state: LoadingState::Idle,
443 loading_started_at: None,
444 }
445 }
446
447 pub fn new_from_parse_result(
451 ecosystem_id: &'static str,
452 content: String,
453 parse_result: Box<dyn ParseResult>,
454 ) -> Self {
455 let ecosystem = match ecosystem_id {
456 "cargo" => Ecosystem::Cargo,
457 "npm" => Ecosystem::Npm,
458 "pypi" => Ecosystem::Pypi,
459 "go" => Ecosystem::Go,
460 _ => Ecosystem::Cargo, };
462
463 Self {
464 ecosystem,
465 ecosystem_id,
466 content,
467 dependencies: vec![],
468 parse_result: Some(parse_result),
469 versions: HashMap::new(),
470 cached_versions: HashMap::new(),
471 resolved_versions: HashMap::new(),
472 parsed_at: Instant::now(),
473 loading_state: LoadingState::Idle,
474 loading_started_at: None,
475 }
476 }
477
478 pub fn new_without_parse_result(ecosystem_id: &'static str, content: String) -> Self {
483 let ecosystem = match ecosystem_id {
484 "cargo" => Ecosystem::Cargo,
485 "npm" => Ecosystem::Npm,
486 "pypi" => Ecosystem::Pypi,
487 "go" => Ecosystem::Go,
488 _ => Ecosystem::Cargo, };
490
491 Self {
492 ecosystem,
493 ecosystem_id,
494 content,
495 dependencies: vec![],
496 parse_result: None,
497 versions: HashMap::new(),
498 cached_versions: HashMap::new(),
499 resolved_versions: HashMap::new(),
500 parsed_at: Instant::now(),
501 loading_state: LoadingState::Idle,
502 loading_started_at: None,
503 }
504 }
505
506 pub fn parse_result(&self) -> Option<&dyn ParseResult> {
508 self.parse_result.as_ref().map(std::convert::AsRef::as_ref)
509 }
510
511 pub fn update_versions(&mut self, versions: HashMap<String, UnifiedVersion>) {
513 self.versions = versions;
514 }
515
516 pub fn update_cached_versions(&mut self, versions: HashMap<String, String>) {
518 self.cached_versions = versions;
519 }
520
521 pub fn update_resolved_versions(&mut self, versions: HashMap<String, String>) {
523 self.resolved_versions = versions;
524 }
525
526 pub fn set_loading(&mut self) {
544 self.loading_state = LoadingState::Loading;
545 self.loading_started_at = Some(Instant::now());
546 }
547
548 pub fn set_loaded(&mut self) {
562 self.loading_state = LoadingState::Loaded;
563 self.loading_started_at = None;
564 }
565
566 pub fn set_failed(&mut self) {
580 self.loading_state = LoadingState::Failed;
581 self.loading_started_at = None;
582 }
583
584 #[must_use]
601 pub fn loading_duration(&self) -> Option<Duration> {
602 self.loading_started_at
603 .map(|start| Instant::now().duration_since(start))
604 }
605}
606
607pub struct ServerState {
624 pub documents: DashMap<Uri, DocumentState>,
626 pub cache: Arc<HttpCache>,
628 pub lockfile_cache: Arc<LockFileCache>,
630 pub ecosystem_registry: Arc<EcosystemRegistry>,
632 pub cold_start_limiter: ColdStartLimiter,
634 tasks: tokio::sync::RwLock<HashMap<Uri, JoinHandle<()>>>,
636}
637
638impl ServerState {
639 pub fn new() -> Self {
641 let cache = Arc::new(HttpCache::new());
642 let lockfile_cache = Arc::new(LockFileCache::new());
643 let ecosystem_registry = Arc::new(EcosystemRegistry::new());
644
645 crate::register_ecosystems(&ecosystem_registry, Arc::clone(&cache));
647
648 let cold_start_limiter = ColdStartLimiter::new(Duration::from_millis(100));
650
651 Self {
652 documents: DashMap::new(),
653 cache,
654 lockfile_cache,
655 ecosystem_registry,
656 cold_start_limiter,
657 tasks: tokio::sync::RwLock::new(HashMap::new()),
658 }
659 }
660
661 pub fn get_document(
667 &self,
668 uri: &Uri,
669 ) -> Option<dashmap::mapref::one::Ref<'_, Uri, DocumentState>> {
670 self.documents.get(uri)
671 }
672
673 pub fn get_document_clone(&self, uri: &Uri) -> Option<DocumentState> {
703 self.documents.get(uri).map(|doc| doc.clone())
704 }
705
706 pub fn update_document(&self, uri: Uri, state: DocumentState) {
711 self.documents.insert(uri, state);
712 }
713
714 pub fn remove_document(&self, uri: &Uri) -> Option<(Uri, DocumentState)> {
718 self.documents.remove(uri)
719 }
720
721 pub async fn spawn_background_task(&self, uri: Uri, task: JoinHandle<()>) {
730 let mut tasks = self.tasks.write().await;
731
732 if let Some(old_task) = tasks.remove(&uri) {
734 old_task.abort();
735 }
736
737 tasks.insert(uri, task);
738 }
739
740 pub async fn cancel_background_task(&self, uri: &Uri) {
744 let mut tasks = self.tasks.write().await;
745 if let Some(task) = tasks.remove(uri) {
746 task.abort();
747 }
748 }
749
750 pub fn document_count(&self) -> usize {
752 self.documents.len()
753 }
754}
755
756impl Default for ServerState {
757 fn default() -> Self {
758 Self::new()
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765
766 mod loading_state_tests {
775 use super::*;
776
777 #[test]
778 fn test_loading_state_default() {
779 let state = LoadingState::default();
780 assert_eq!(state, LoadingState::Idle);
781 }
782
783 #[test]
784 fn test_loading_state_transitions() {
785 use std::time::Duration;
786
787 let content = "[dependencies]\nserde = \"1.0\"".to_string();
788 let mut doc = DocumentState::new_without_parse_result("cargo", content);
789
790 assert_eq!(doc.loading_state, LoadingState::Idle);
792 assert!(doc.loading_started_at.is_none());
793
794 doc.set_loading();
796 assert_eq!(doc.loading_state, LoadingState::Loading);
797 assert!(doc.loading_started_at.is_some());
798
799 std::thread::sleep(Duration::from_millis(10));
801
802 let duration = doc.loading_duration();
804 assert!(duration.is_some());
805 assert!(duration.unwrap() >= Duration::from_millis(10));
806
807 doc.set_loaded();
809 assert_eq!(doc.loading_state, LoadingState::Loaded);
810 assert!(doc.loading_started_at.is_none());
811 assert!(doc.loading_duration().is_none());
812 }
813
814 #[test]
815 fn test_loading_state_failed_transition() {
816 let content = "[dependencies]\nserde = \"1.0\"".to_string();
817 let mut doc = DocumentState::new_without_parse_result("cargo", content);
818
819 doc.set_loading();
820 assert_eq!(doc.loading_state, LoadingState::Loading);
821
822 doc.set_failed();
823 assert_eq!(doc.loading_state, LoadingState::Failed);
824 assert!(doc.loading_started_at.is_none());
825 }
826
827 #[test]
828 fn test_loading_state_clone() {
829 let content = "[dependencies]\nserde = \"1.0\"".to_string();
830 let mut doc = DocumentState::new_without_parse_result("cargo", content);
831
832 doc.set_loading();
833 let cloned = doc.clone();
834
835 assert_eq!(cloned.loading_state, LoadingState::Loading);
836 assert!(cloned.loading_started_at.is_some());
837 }
838
839 #[test]
840 fn test_loading_state_debug() {
841 let content = "[dependencies]\nserde = \"1.0\"".to_string();
842 let mut doc = DocumentState::new_without_parse_result("cargo", content);
843 doc.set_loading();
844
845 let debug_str = format!("{:?}", doc);
846 assert!(debug_str.contains("loading_state"));
847 assert!(debug_str.contains("Loading"));
848 }
849
850 #[test]
851 fn test_loading_duration_none_when_idle() {
852 let content = "[dependencies]\nserde = \"1.0\"".to_string();
853 let doc = DocumentState::new_without_parse_result("cargo", content);
854
855 assert_eq!(doc.loading_state, LoadingState::Idle);
856 assert!(doc.loading_duration().is_none());
857 }
858
859 #[test]
860 fn test_loading_state_equality() {
861 assert_eq!(LoadingState::Idle, LoadingState::Idle);
862 assert_eq!(LoadingState::Loading, LoadingState::Loading);
863 assert_eq!(LoadingState::Loaded, LoadingState::Loaded);
864 assert_eq!(LoadingState::Failed, LoadingState::Failed);
865
866 assert_ne!(LoadingState::Idle, LoadingState::Loading);
867 assert_ne!(LoadingState::Loading, LoadingState::Loaded);
868 }
869
870 #[test]
871 fn test_loading_duration_tracks_time_correctly() {
872 use std::time::Duration;
873
874 let content = "[dependencies]\nserde = \"1.0\"".to_string();
875 let mut doc = DocumentState::new_without_parse_result("cargo", content);
876
877 doc.set_loading();
878
879 let duration1 = doc.loading_duration().unwrap();
881 std::thread::sleep(Duration::from_millis(20));
882 let duration2 = doc.loading_duration().unwrap();
883
884 assert!(duration2 > duration1, "Duration should increase over time");
885 }
886
887 #[tokio::test]
888 async fn test_concurrent_loading_state_mutations() {
889 use std::sync::Arc;
890 use tokio::sync::Barrier;
891
892 let state = Arc::new(ServerState::new());
893 let uri = Uri::from_file_path("/concurrent-loading-test.toml").unwrap();
894
895 let doc = DocumentState::new_without_parse_result("cargo", String::new());
896 state.update_document(uri.clone(), doc);
897
898 let barrier = Arc::new(Barrier::new(10));
899 let mut handles = vec![];
900
901 for i in 0..10 {
902 let state_clone = Arc::clone(&state);
903 let uri_clone = uri.clone();
904 let barrier_clone = Arc::clone(&barrier);
905
906 handles.push(tokio::spawn(async move {
907 barrier_clone.wait().await;
908 if let Some(mut doc) = state_clone.documents.get_mut(&uri_clone) {
909 if i % 3 == 0 {
910 doc.set_loading();
911 } else if i % 3 == 1 {
912 doc.set_loaded();
913 } else {
914 doc.set_failed();
915 }
916 }
917 }));
918 }
919
920 for handle in handles {
921 handle.await.unwrap();
922 }
923
924 let doc = state.get_document(&uri).unwrap();
925 assert!(matches!(
926 doc.loading_state,
927 LoadingState::Idle
928 | LoadingState::Loading
929 | LoadingState::Loaded
930 | LoadingState::Failed
931 ));
932 }
933
934 #[test]
935 fn test_set_loaded_idempotent() {
936 let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
937
938 doc.set_loading();
939 doc.set_loaded();
940
941 doc.set_loaded();
943
944 assert_eq!(doc.loading_state, LoadingState::Loaded);
945 assert!(doc.loading_started_at.is_none());
946 }
947
948 #[test]
949 fn test_set_loading_resets_timer() {
950 let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
951
952 doc.set_loading();
953 let first_start = doc.loading_started_at.unwrap();
954
955 std::thread::sleep(std::time::Duration::from_millis(10));
956
957 doc.set_loading();
959 let second_start = doc.loading_started_at.unwrap();
960
961 assert!(second_start > first_start, "Timer should be reset");
962 assert_eq!(doc.loading_state, LoadingState::Loading);
963 }
964
965 #[test]
966 fn test_retry_after_failure() {
967 let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
968
969 doc.set_loading();
970 doc.set_failed();
971 assert_eq!(doc.loading_state, LoadingState::Failed);
972 assert!(doc.loading_started_at.is_none());
973
974 doc.set_loading();
976 assert_eq!(doc.loading_state, LoadingState::Loading);
977 assert!(doc.loading_started_at.is_some());
978
979 doc.set_loaded();
980 assert_eq!(doc.loading_state, LoadingState::Loaded);
981 }
982
983 #[test]
984 fn test_refresh_after_loaded() {
985 let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
986
987 doc.set_loading();
988 doc.set_loaded();
989 assert_eq!(doc.loading_state, LoadingState::Loaded);
990
991 doc.set_loading();
993 assert_eq!(doc.loading_state, LoadingState::Loading);
994 assert!(doc.loading_started_at.is_some());
995
996 doc.set_loaded();
997 assert_eq!(doc.loading_state, LoadingState::Loaded);
998 }
999 }
1000
1001 #[test]
1002 fn test_ecosystem_from_filename() {
1003 #[cfg(feature = "cargo")]
1004 assert_eq!(
1005 Ecosystem::from_filename("Cargo.toml"),
1006 Some(Ecosystem::Cargo)
1007 );
1008 #[cfg(feature = "npm")]
1009 assert_eq!(
1010 Ecosystem::from_filename("package.json"),
1011 Some(Ecosystem::Npm)
1012 );
1013 #[cfg(feature = "pypi")]
1014 assert_eq!(
1015 Ecosystem::from_filename("pyproject.toml"),
1016 Some(Ecosystem::Pypi)
1017 );
1018 #[cfg(feature = "go")]
1019 assert_eq!(Ecosystem::from_filename("go.mod"), Some(Ecosystem::Go));
1020 assert_eq!(Ecosystem::from_filename("unknown.txt"), None);
1021 }
1022
1023 #[test]
1024 fn test_ecosystem_from_uri() {
1025 #[cfg(feature = "cargo")]
1026 {
1027 let cargo_uri = Uri::from_file_path("/path/to/Cargo.toml").unwrap();
1028 assert_eq!(Ecosystem::from_uri(&cargo_uri), Some(Ecosystem::Cargo));
1029 }
1030 #[cfg(feature = "npm")]
1031 {
1032 let npm_uri = Uri::from_file_path("/path/to/package.json").unwrap();
1033 assert_eq!(Ecosystem::from_uri(&npm_uri), Some(Ecosystem::Npm));
1034 }
1035 #[cfg(feature = "pypi")]
1036 {
1037 let pypi_uri = Uri::from_file_path("/path/to/pyproject.toml").unwrap();
1038 assert_eq!(Ecosystem::from_uri(&pypi_uri), Some(Ecosystem::Pypi));
1039 }
1040 #[cfg(feature = "go")]
1041 {
1042 let go_uri = Uri::from_file_path("/path/to/go.mod").unwrap();
1043 assert_eq!(Ecosystem::from_uri(&go_uri), Some(Ecosystem::Go));
1044 }
1045 let unknown_uri = Uri::from_file_path("/path/to/README.md").unwrap();
1046 assert_eq!(Ecosystem::from_uri(&unknown_uri), None);
1047 }
1048
1049 #[test]
1050 fn test_ecosystem_from_filename_edge_cases() {
1051 assert_eq!(Ecosystem::from_filename(""), None);
1052 assert_eq!(Ecosystem::from_filename("cargo.toml"), None);
1053 assert_eq!(Ecosystem::from_filename("CARGO.TOML"), None);
1054 assert_eq!(Ecosystem::from_filename("requirements.txt"), None);
1055 }
1056
1057 #[test]
1058 fn test_server_state_creation() {
1059 let state = ServerState::new();
1060 assert_eq!(state.document_count(), 0);
1061 assert!(state.cache.is_empty(), "Cache should start empty");
1062 }
1063
1064 #[test]
1065 fn test_server_state_default() {
1066 let state = ServerState::default();
1067 assert_eq!(state.document_count(), 0);
1068 }
1069
1070 #[tokio::test]
1071 async fn test_server_state_background_tasks() {
1072 let state = ServerState::new();
1073 let uri = Uri::from_file_path("/test.toml").unwrap();
1074
1075 let task = tokio::spawn(async {
1076 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1077 });
1078
1079 state.spawn_background_task(uri.clone(), task).await;
1080 state.cancel_background_task(&uri).await;
1081 }
1082
1083 #[tokio::test]
1084 async fn test_spawn_background_task_cancels_previous() {
1085 let state = ServerState::new();
1086 let uri = Uri::from_file_path("/test.toml").unwrap();
1087
1088 let task1 = tokio::spawn(async {
1089 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1090 });
1091 state.spawn_background_task(uri.clone(), task1).await;
1092
1093 let task2 = tokio::spawn(async {
1094 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1095 });
1096 state.spawn_background_task(uri.clone(), task2).await;
1097 state.cancel_background_task(&uri).await;
1098 }
1099
1100 #[tokio::test]
1101 async fn test_cancel_background_task_nonexistent() {
1102 let state = ServerState::new();
1103 let uri = Uri::from_file_path("/test.toml").unwrap();
1104 state.cancel_background_task(&uri).await;
1105 }
1106
1107 mod cold_start_limiter {
1112 use super::*;
1113 use std::time::Duration;
1114
1115 #[test]
1116 fn test_allows_first_request() {
1117 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1118 let uri = Uri::from_file_path("/test.toml").unwrap();
1119 assert!(
1120 limiter.allow_cold_start(&uri),
1121 "First request should be allowed"
1122 );
1123 }
1124
1125 #[test]
1126 fn test_blocks_rapid_requests() {
1127 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1128 let uri = Uri::from_file_path("/test.toml").unwrap();
1129
1130 assert!(limiter.allow_cold_start(&uri), "First request allowed");
1131 assert!(
1132 !limiter.allow_cold_start(&uri),
1133 "Second immediate request should be blocked"
1134 );
1135 }
1136
1137 #[tokio::test]
1138 async fn test_allows_after_interval() {
1139 let limiter = ColdStartLimiter::new(Duration::from_millis(50));
1140 let uri = Uri::from_file_path("/test.toml").unwrap();
1141
1142 assert!(limiter.allow_cold_start(&uri), "First request allowed");
1143 tokio::time::sleep(Duration::from_millis(60)).await;
1144 assert!(
1145 limiter.allow_cold_start(&uri),
1146 "Request after interval should be allowed"
1147 );
1148 }
1149
1150 #[test]
1151 fn test_different_uris_independent() {
1152 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1153 let uri1 = Uri::from_file_path("/test1.toml").unwrap();
1154 let uri2 = Uri::from_file_path("/test2.toml").unwrap();
1155
1156 assert!(limiter.allow_cold_start(&uri1), "URI 1 first request");
1157 assert!(limiter.allow_cold_start(&uri2), "URI 2 first request");
1158 assert!(
1159 !limiter.allow_cold_start(&uri1),
1160 "URI 1 second request blocked"
1161 );
1162 assert!(
1163 !limiter.allow_cold_start(&uri2),
1164 "URI 2 second request blocked"
1165 );
1166 }
1167
1168 #[test]
1169 fn test_cleanup() {
1170 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1171 let uri1 = Uri::from_file_path("/test1.toml").unwrap();
1172 let uri2 = Uri::from_file_path("/test2.toml").unwrap();
1173
1174 limiter.allow_cold_start(&uri1);
1175 limiter.allow_cold_start(&uri2);
1176 assert_eq!(limiter.tracked_count(), 2, "Should track 2 URIs");
1177
1178 limiter.cleanup_old_entries(Duration::from_millis(0));
1179 assert_eq!(
1180 limiter.tracked_count(),
1181 0,
1182 "All entries should be cleaned up"
1183 );
1184 }
1185
1186 #[tokio::test]
1187 async fn test_concurrent_access() {
1188 use std::sync::Arc;
1189
1190 let limiter = Arc::new(ColdStartLimiter::new(Duration::from_millis(100)));
1191 let uri = Uri::from_file_path("/concurrent-test.toml").unwrap();
1192
1193 let mut handles = vec![];
1194 const CONCURRENT_TASKS: usize = 10;
1195
1196 for _ in 0..CONCURRENT_TASKS {
1197 let limiter_clone = Arc::clone(&limiter);
1198 let uri_clone = uri.clone();
1199 let handle =
1200 tokio::spawn(async move { limiter_clone.allow_cold_start(&uri_clone) });
1201 handles.push(handle);
1202 }
1203
1204 let mut results = vec![];
1205 for handle in handles {
1206 results.push(handle.await.unwrap());
1207 }
1208
1209 let allowed_count = results.iter().filter(|&&allowed| allowed).count();
1210 assert_eq!(allowed_count, 1, "Exactly one concurrent request allowed");
1211
1212 let blocked_count = results.iter().filter(|&&allowed| !allowed).count();
1213 assert_eq!(
1214 blocked_count,
1215 CONCURRENT_TASKS - 1,
1216 "Rest should be blocked"
1217 );
1218 }
1219 }
1220
1221 #[cfg(feature = "cargo")]
1226 mod cargo_tests {
1227 use super::*;
1228 use deps_cargo::{DependencySection, DependencySource};
1229 use tower_lsp_server::ls_types::{Position, Range};
1230
1231 fn create_test_dependency() -> UnifiedDependency {
1232 UnifiedDependency::Cargo(ParsedDependency {
1233 name: "serde".into(),
1234 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1235 version_req: Some("1.0".into()),
1236 version_range: Some(Range::new(Position::new(0, 9), Position::new(0, 14))),
1237 features: vec![],
1238 features_range: None,
1239 source: DependencySource::Registry,
1240 section: DependencySection::Dependencies,
1241 })
1242 }
1243
1244 #[test]
1245 fn test_document_state_creation() {
1246 let deps = vec![create_test_dependency()];
1247 let state = DocumentState::new(Ecosystem::Cargo, "test content".into(), deps);
1248
1249 assert_eq!(state.ecosystem, Ecosystem::Cargo);
1250 assert_eq!(state.content, "test content");
1251 assert_eq!(state.dependencies.len(), 1);
1252 assert!(state.versions.is_empty());
1253 }
1254
1255 #[test]
1256 fn test_document_state_update_versions() {
1257 let deps = vec![create_test_dependency()];
1258 let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1259
1260 let mut versions = HashMap::new();
1261 versions.insert(
1262 "serde".into(),
1263 UnifiedVersion::Cargo(CargoVersion {
1264 num: "1.0.0".into(),
1265 yanked: false,
1266 features: HashMap::new(),
1267 }),
1268 );
1269
1270 state.update_versions(versions);
1271 assert_eq!(state.versions.len(), 1);
1272 assert!(state.versions.contains_key("serde"));
1273 }
1274
1275 #[test]
1276 fn test_server_state_document_operations() {
1277 let state = ServerState::new();
1278 let uri = Uri::from_file_path("/test.toml").unwrap();
1279 let deps = vec![create_test_dependency()];
1280 let doc_state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1281
1282 state.update_document(uri.clone(), doc_state);
1283 assert_eq!(state.document_count(), 1);
1284
1285 let retrieved = state.get_document(&uri);
1286 assert!(retrieved.is_some());
1287 assert_eq!(retrieved.unwrap().content, "test");
1288
1289 let removed = state.remove_document(&uri);
1290 assert!(removed.is_some());
1291 assert_eq!(state.document_count(), 0);
1292 }
1293
1294 #[test]
1295 fn test_unified_dependency_name() {
1296 let cargo_dep = create_test_dependency();
1297 assert_eq!(cargo_dep.name(), "serde");
1298 assert_eq!(cargo_dep.version_req(), Some("1.0"));
1299 assert!(cargo_dep.is_registry());
1300 }
1301
1302 #[test]
1303 fn test_unified_dependency_git_source() {
1304 let git_dep = UnifiedDependency::Cargo(ParsedDependency {
1305 name: "custom".into(),
1306 name_range: Range::new(Position::new(0, 0), Position::new(0, 6)),
1307 version_req: None,
1308 version_range: None,
1309 features: vec![],
1310 features_range: None,
1311 source: DependencySource::Git {
1312 url: "https://github.com/user/repo".into(),
1313 rev: None,
1314 },
1315 section: DependencySection::Dependencies,
1316 });
1317 assert!(!git_dep.is_registry());
1318 }
1319
1320 #[test]
1321 fn test_unified_dependency_workspace_source() {
1322 let ws_dep = UnifiedDependency::Cargo(ParsedDependency {
1323 name: "serde".into(),
1324 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1325 version_req: None,
1326 version_range: None,
1327 features: vec![],
1328 features_range: None,
1329 source: DependencySource::Workspace,
1330 section: DependencySection::Dependencies,
1331 });
1332 assert!(!ws_dep.is_registry());
1333 }
1334
1335 #[test]
1336 fn test_unified_version() {
1337 let version = UnifiedVersion::Cargo(CargoVersion {
1338 num: "1.0.0".into(),
1339 yanked: false,
1340 features: HashMap::new(),
1341 });
1342 assert_eq!(version.version_string(), "1.0.0");
1343 assert!(!version.is_yanked());
1344 }
1345
1346 #[test]
1347 fn test_document_state_new_from_parse_result() {
1348 let state = ServerState::new();
1349 let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
1350 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
1351 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1352
1353 let parse_result = tokio::runtime::Runtime::new()
1354 .unwrap()
1355 .block_on(ecosystem.parse_manifest(&content, &uri))
1356 .unwrap();
1357
1358 let doc_state =
1359 DocumentState::new_from_parse_result("cargo", content.clone(), parse_result);
1360
1361 assert_eq!(doc_state.ecosystem_id, "cargo");
1362 assert_eq!(doc_state.content, content);
1363 assert!(doc_state.parse_result.is_some());
1364 }
1365
1366 #[test]
1367 fn test_document_state_new_without_parse_result() {
1368 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1369 let doc_state = DocumentState::new_without_parse_result("cargo", content);
1370
1371 assert_eq!(doc_state.ecosystem_id, "cargo");
1372 assert_eq!(doc_state.ecosystem, Ecosystem::Cargo);
1373 assert!(doc_state.parse_result.is_none());
1374 assert!(doc_state.dependencies.is_empty());
1375 }
1376
1377 #[test]
1378 fn test_document_state_update_resolved_versions() {
1379 let deps = vec![create_test_dependency()];
1380 let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1381
1382 let mut resolved = HashMap::new();
1383 resolved.insert("serde".into(), "1.0.195".into());
1384
1385 state.update_resolved_versions(resolved);
1386 assert_eq!(state.resolved_versions.len(), 1);
1387 assert_eq!(
1388 state.resolved_versions.get("serde"),
1389 Some(&"1.0.195".into())
1390 );
1391 }
1392
1393 #[test]
1394 fn test_document_state_update_cached_versions() {
1395 let deps = vec![create_test_dependency()];
1396 let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1397
1398 let mut cached = HashMap::new();
1399 cached.insert("serde".into(), "1.0.210".into());
1400
1401 state.update_cached_versions(cached);
1402 assert_eq!(state.cached_versions.len(), 1);
1403 }
1404
1405 #[test]
1406 fn test_document_state_parse_result_accessor() {
1407 let deps = vec![create_test_dependency()];
1408 let state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1409 assert!(state.parse_result().is_none());
1410 }
1411
1412 #[test]
1413 fn test_document_state_clone() {
1414 let deps = vec![create_test_dependency()];
1415 let state = DocumentState::new(Ecosystem::Cargo, "test content".into(), deps);
1416 let cloned = state.clone();
1417
1418 assert_eq!(cloned.ecosystem, state.ecosystem);
1419 assert_eq!(cloned.content, state.content);
1420 assert_eq!(cloned.dependencies.len(), state.dependencies.len());
1421 assert!(cloned.parse_result.is_none());
1422 }
1423
1424 #[test]
1425 fn test_document_state_debug() {
1426 let deps = vec![create_test_dependency()];
1427 let state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1428 let debug_str = format!("{state:?}");
1429 assert!(debug_str.contains("DocumentState"));
1430 }
1431 }
1432
1433 #[cfg(feature = "npm")]
1438 mod npm_tests {
1439 use super::*;
1440 use deps_npm::{NpmDependency, NpmDependencySection};
1441 use tower_lsp_server::ls_types::{Position, Range};
1442
1443 #[test]
1444 fn test_unified_dependency() {
1445 let npm_dep = UnifiedDependency::Npm(NpmDependency {
1446 name: "express".into(),
1447 name_range: Range::new(Position::new(0, 0), Position::new(0, 7)),
1448 version_req: Some("^4.0.0".into()),
1449 version_range: Some(Range::new(Position::new(0, 11), Position::new(0, 18))),
1450 section: NpmDependencySection::Dependencies,
1451 });
1452
1453 assert_eq!(npm_dep.name(), "express");
1454 assert_eq!(npm_dep.version_req(), Some("^4.0.0"));
1455 assert!(npm_dep.is_registry());
1456 }
1457
1458 #[test]
1459 fn test_unified_version() {
1460 let version = UnifiedVersion::Npm(deps_npm::NpmVersion {
1461 version: "4.18.2".into(),
1462 deprecated: false,
1463 });
1464 assert_eq!(version.version_string(), "4.18.2");
1465 assert!(!version.is_yanked());
1466 }
1467
1468 #[test]
1469 fn test_document_state_new_without_parse_result() {
1470 let content = r#"{"dependencies": {"express": "^4.18.0"}}"#.to_string();
1471 let doc_state = DocumentState::new_without_parse_result("npm", content);
1472
1473 assert_eq!(doc_state.ecosystem_id, "npm");
1474 assert_eq!(doc_state.ecosystem, Ecosystem::Npm);
1475 assert!(doc_state.parse_result.is_none());
1476 }
1477 }
1478
1479 #[cfg(feature = "pypi")]
1484 mod pypi_tests {
1485 use super::*;
1486 use deps_pypi::{PypiDependency, PypiDependencySection, PypiDependencySource};
1487 use tower_lsp_server::ls_types::{Position, Range};
1488
1489 #[test]
1490 fn test_unified_dependency() {
1491 let pypi_dep = UnifiedDependency::Pypi(PypiDependency {
1492 name: "requests".into(),
1493 name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
1494 version_req: Some(">=2.0.0".into()),
1495 version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 18))),
1496 extras: vec![],
1497 extras_range: None,
1498 markers: None,
1499 markers_range: None,
1500 source: PypiDependencySource::Registry,
1501 section: PypiDependencySection::Dependencies,
1502 });
1503
1504 assert_eq!(pypi_dep.name(), "requests");
1505 assert_eq!(pypi_dep.version_req(), Some(">=2.0.0"));
1506 assert!(pypi_dep.is_registry());
1507 }
1508
1509 #[test]
1510 fn test_unified_version() {
1511 let version = UnifiedVersion::Pypi(deps_pypi::PypiVersion {
1512 version: "2.31.0".into(),
1513 yanked: true,
1514 });
1515 assert_eq!(version.version_string(), "2.31.0");
1516 assert!(version.is_yanked());
1517 }
1518
1519 #[test]
1520 fn test_document_state_new_without_parse_result() {
1521 let content = "[project]\ndependencies = [\"requests>=2.0.0\"]\n".to_string();
1522 let doc_state = DocumentState::new_without_parse_result("pypi", content);
1523
1524 assert_eq!(doc_state.ecosystem_id, "pypi");
1525 assert_eq!(doc_state.ecosystem, Ecosystem::Pypi);
1526 assert!(doc_state.parse_result.is_none());
1527 }
1528 }
1529
1530 #[cfg(feature = "go")]
1535 mod go_tests {
1536 use super::*;
1537 use deps_go::{GoDependency, GoDirective, GoVersion};
1538 use tower_lsp_server::ls_types::{Position, Range};
1539
1540 fn create_test_dependency() -> UnifiedDependency {
1541 UnifiedDependency::Go(GoDependency {
1542 module_path: "github.com/gin-gonic/gin".into(),
1543 module_path_range: Range::new(Position::new(0, 0), Position::new(0, 25)),
1544 version: Some("v1.9.1".into()),
1545 version_range: Some(Range::new(Position::new(0, 26), Position::new(0, 32))),
1546 directive: GoDirective::Require,
1547 indirect: false,
1548 })
1549 }
1550
1551 #[test]
1552 fn test_unified_dependency() {
1553 let go_dep = create_test_dependency();
1554 assert_eq!(go_dep.name(), "github.com/gin-gonic/gin");
1555 assert_eq!(go_dep.version_req(), Some("v1.9.1"));
1556 assert!(go_dep.is_registry());
1557 }
1558
1559 #[test]
1560 fn test_unified_dependency_name_range() {
1561 let range = Range::new(Position::new(5, 10), Position::new(5, 35));
1562 let go_dep = UnifiedDependency::Go(GoDependency {
1563 module_path: "github.com/example/pkg".into(),
1564 module_path_range: range,
1565 version: Some("v1.0.0".into()),
1566 version_range: Some(Range::new(Position::new(5, 36), Position::new(5, 42))),
1567 directive: GoDirective::Require,
1568 indirect: false,
1569 });
1570 assert_eq!(go_dep.name_range(), range);
1571 }
1572
1573 #[test]
1574 fn test_unified_dependency_version_range() {
1575 let version_range = Range::new(Position::new(5, 36), Position::new(5, 42));
1576 let go_dep = UnifiedDependency::Go(GoDependency {
1577 module_path: "github.com/example/pkg".into(),
1578 module_path_range: Range::new(Position::new(5, 10), Position::new(5, 35)),
1579 version: Some("v1.0.0".into()),
1580 version_range: Some(version_range),
1581 directive: GoDirective::Require,
1582 indirect: false,
1583 });
1584 assert_eq!(go_dep.version_range(), Some(version_range));
1585 }
1586
1587 #[test]
1588 fn test_unified_dependency_no_version() {
1589 let go_dep = UnifiedDependency::Go(GoDependency {
1590 module_path: "github.com/example/pkg".into(),
1591 module_path_range: Range::new(Position::new(5, 10), Position::new(5, 35)),
1592 version: None,
1593 version_range: None,
1594 directive: GoDirective::Require,
1595 indirect: false,
1596 });
1597 assert_eq!(go_dep.version_req(), None);
1598 assert_eq!(go_dep.version_range(), None);
1599 }
1600
1601 #[test]
1602 fn test_unified_version() {
1603 let version = UnifiedVersion::Go(GoVersion {
1604 version: "v1.9.1".into(),
1605 time: Some("2023-07-18T14:30:00Z".into()),
1606 is_pseudo: false,
1607 retracted: false,
1608 });
1609 assert_eq!(version.version_string(), "v1.9.1");
1610 assert!(!version.is_yanked());
1611 }
1612
1613 #[test]
1614 fn test_unified_version_retracted() {
1615 let version = UnifiedVersion::Go(GoVersion {
1616 version: "v1.0.0".into(),
1617 time: None,
1618 is_pseudo: false,
1619 retracted: true,
1620 });
1621 assert_eq!(version.version_string(), "v1.0.0");
1622 assert!(version.is_yanked());
1623 }
1624
1625 #[test]
1626 fn test_unified_version_pseudo() {
1627 let version = UnifiedVersion::Go(GoVersion {
1628 version: "v0.0.0-20191109021931-daa7c04131f5".into(),
1629 time: Some("2019-11-09T02:19:31Z".into()),
1630 is_pseudo: true,
1631 retracted: false,
1632 });
1633 assert_eq!(
1634 version.version_string(),
1635 "v0.0.0-20191109021931-daa7c04131f5"
1636 );
1637 assert!(!version.is_yanked());
1638 }
1639
1640 #[test]
1641 fn test_document_state_new() {
1642 let deps = vec![create_test_dependency()];
1643 let state = DocumentState::new(Ecosystem::Go, "test content".into(), deps);
1644
1645 assert_eq!(state.ecosystem, Ecosystem::Go);
1646 assert_eq!(state.ecosystem_id, "go");
1647 assert_eq!(state.dependencies.len(), 1);
1648 }
1649
1650 #[test]
1651 fn test_document_state_new_without_parse_result() {
1652 let content =
1653 "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1654 .to_string();
1655 let doc_state = DocumentState::new_without_parse_result("go", content);
1656
1657 assert_eq!(doc_state.ecosystem_id, "go");
1658 assert_eq!(doc_state.ecosystem, Ecosystem::Go);
1659 assert!(doc_state.parse_result.is_none());
1660 }
1661
1662 #[test]
1663 fn test_document_state_new_from_parse_result() {
1664 let state = ServerState::new();
1665 let uri = Uri::from_file_path("/test/go.mod").unwrap();
1666 let ecosystem = state.ecosystem_registry.get("go").unwrap();
1667 let content =
1668 "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1669 .to_string();
1670
1671 let parse_result = tokio::runtime::Runtime::new()
1672 .unwrap()
1673 .block_on(ecosystem.parse_manifest(&content, &uri))
1674 .unwrap();
1675
1676 let doc_state =
1677 DocumentState::new_from_parse_result("go", content.clone(), parse_result);
1678
1679 assert_eq!(doc_state.ecosystem_id, "go");
1680 assert!(doc_state.parse_result.is_some());
1681 }
1682 }
1683}