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 match self {
106 #[cfg(feature = "cargo")]
107 Self::Cargo(dep) => {
108 matches!(dep.source, deps_cargo::DependencySource::Registry)
109 }
110 #[cfg(feature = "npm")]
111 Self::Npm(_) => true,
112 #[cfg(feature = "pypi")]
113 Self::Pypi(dep) => {
114 matches!(dep.source, deps_pypi::PypiDependencySource::PyPI)
115 }
116 #[cfg(feature = "go")]
117 Self::Go(_) => true,
118 _ => unreachable!("no ecosystem features enabled"),
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
127#[non_exhaustive]
128pub enum UnifiedVersion {
129 #[cfg(feature = "cargo")]
130 Cargo(CargoVersion),
131 #[cfg(feature = "npm")]
132 Npm(NpmVersion),
133 #[cfg(feature = "pypi")]
134 Pypi(PypiVersion),
135 #[cfg(feature = "go")]
136 Go(GoVersion),
137}
138
139impl UnifiedVersion {
140 #[allow(unreachable_patterns)]
142 pub fn version_string(&self) -> &str {
143 match self {
144 #[cfg(feature = "cargo")]
145 Self::Cargo(v) => &v.num,
146 #[cfg(feature = "npm")]
147 Self::Npm(v) => &v.version,
148 #[cfg(feature = "pypi")]
149 Self::Pypi(v) => &v.version,
150 #[cfg(feature = "go")]
151 Self::Go(v) => &v.version,
152 _ => unreachable!("no ecosystem features enabled"),
153 }
154 }
155
156 #[allow(unreachable_patterns)]
158 pub fn is_yanked(&self) -> bool {
159 match self {
160 #[cfg(feature = "cargo")]
161 Self::Cargo(v) => v.yanked,
162 #[cfg(feature = "npm")]
163 Self::Npm(v) => v.deprecated,
164 #[cfg(feature = "pypi")]
165 Self::Pypi(v) => v.yanked,
166 #[cfg(feature = "go")]
167 Self::Go(v) => v.retracted,
168 _ => unreachable!("no ecosystem features enabled"),
169 }
170 }
171}
172
173impl deps_core::VersionStringGetter for UnifiedVersion {
175 fn version_string(&self) -> &str {
176 self.version_string()
177 }
178}
179
180impl deps_core::YankedChecker for UnifiedVersion {
181 fn is_yanked(&self) -> bool {
182 self.is_yanked()
183 }
184}
185
186pub use deps_core::LoadingState;
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213#[non_exhaustive]
214pub enum Ecosystem {
215 Cargo,
217 Npm,
219 Pypi,
221 Go,
223}
224
225impl Ecosystem {
226 pub fn from_filename(filename: &str) -> Option<Self> {
231 match filename {
232 "Cargo.toml" => Some(Self::Cargo),
233 "package.json" => Some(Self::Npm),
234 "pyproject.toml" => Some(Self::Pypi),
235 "go.mod" => Some(Self::Go),
236 _ => None,
237 }
238 }
239
240 pub fn from_uri(uri: &Uri) -> Option<Self> {
244 let path = uri.path();
245 let filename = path.as_str().split('/').next_back()?;
246 Self::from_filename(filename)
247 }
248}
249
250pub struct DocumentState {
289 pub ecosystem: Ecosystem,
291 pub ecosystem_id: &'static str,
293 pub content: String,
295 pub dependencies: Vec<UnifiedDependency>,
297 #[allow(dead_code)]
300 parse_result: Option<Box<dyn ParseResult>>,
301 pub versions: HashMap<String, UnifiedVersion>,
303 pub cached_versions: HashMap<String, String>,
305 pub resolved_versions: HashMap<String, String>,
307 pub parsed_at: Instant,
309 pub loading_state: LoadingState,
311 pub loading_started_at: Option<Instant>,
313}
314
315impl Clone for DocumentState {
316 fn clone(&self) -> Self {
317 Self {
318 ecosystem: self.ecosystem,
319 ecosystem_id: self.ecosystem_id,
320 content: self.content.clone(),
321 dependencies: self.dependencies.clone(),
322 parse_result: None, versions: self.versions.clone(),
324 cached_versions: self.cached_versions.clone(),
325 resolved_versions: self.resolved_versions.clone(),
326 parsed_at: self.parsed_at,
327 loading_state: self.loading_state,
328 loading_started_at: self.loading_started_at,
330 }
331 }
332}
333
334#[derive(Debug)]
355pub struct ColdStartLimiter {
356 last_attempts: DashMap<Uri, Instant>,
358 min_interval: Duration,
360}
361
362impl ColdStartLimiter {
363 pub fn new(min_interval: Duration) -> Self {
365 Self {
366 last_attempts: DashMap::new(),
367 min_interval,
368 }
369 }
370
371 pub fn allow_cold_start(&self, uri: &Uri) -> bool {
375 let now = Instant::now();
376
377 if let Some(mut entry) = self.last_attempts.get_mut(uri) {
379 let elapsed = now.duration_since(*entry);
380 if elapsed < self.min_interval {
381 let retry_after = self.min_interval.checked_sub(elapsed).unwrap();
382 tracing::warn!(
383 "Cold start rate limited for {:?} (retry after {:?})",
384 uri,
385 retry_after
386 );
387 return false;
388 }
389 *entry = now;
390 } else {
391 self.last_attempts.insert(uri.clone(), now);
392 }
393
394 true
395 }
396
397 pub fn cleanup_old_entries(&self, max_age: Duration) {
402 let now = Instant::now();
403 self.last_attempts
404 .retain(|_, instant| now.duration_since(*instant) < max_age);
405 }
406
407 #[cfg(test)]
409 pub fn tracked_count(&self) -> usize {
410 self.last_attempts.len()
411 }
412}
413
414impl std::fmt::Debug for DocumentState {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 f.debug_struct("DocumentState")
417 .field("ecosystem", &self.ecosystem)
418 .field("ecosystem_id", &self.ecosystem_id)
419 .field("content_len", &self.content.len())
420 .field("dependencies_count", &self.dependencies.len())
421 .field("has_parse_result", &self.parse_result.is_some())
422 .field("versions_count", &self.versions.len())
423 .field("cached_versions_count", &self.cached_versions.len())
424 .field("resolved_versions_count", &self.resolved_versions.len())
425 .field("parsed_at", &self.parsed_at)
426 .field("loading_state", &self.loading_state)
427 .field("loading_started_at", &self.loading_started_at)
428 .finish()
429 }
430}
431
432impl DocumentState {
433 pub fn new(
438 ecosystem: Ecosystem,
439 content: String,
440 dependencies: Vec<UnifiedDependency>,
441 ) -> Self {
442 let ecosystem_id = match ecosystem {
443 Ecosystem::Cargo => "cargo",
444 Ecosystem::Npm => "npm",
445 Ecosystem::Pypi => "pypi",
446 Ecosystem::Go => "go",
447 };
448
449 Self {
450 ecosystem,
451 ecosystem_id,
452 content,
453 dependencies,
454 parse_result: None,
455 versions: HashMap::new(),
456 cached_versions: HashMap::new(),
457 resolved_versions: HashMap::new(),
458 parsed_at: Instant::now(),
459 loading_state: LoadingState::Idle,
460 loading_started_at: None,
461 }
462 }
463
464 pub fn new_from_parse_result(
468 ecosystem_id: &'static str,
469 content: String,
470 parse_result: Box<dyn ParseResult>,
471 ) -> Self {
472 let ecosystem = match ecosystem_id {
473 "cargo" => Ecosystem::Cargo,
474 "npm" => Ecosystem::Npm,
475 "pypi" => Ecosystem::Pypi,
476 "go" => Ecosystem::Go,
477 _ => Ecosystem::Cargo, };
479
480 Self {
481 ecosystem,
482 ecosystem_id,
483 content,
484 dependencies: vec![],
485 parse_result: Some(parse_result),
486 versions: HashMap::new(),
487 cached_versions: HashMap::new(),
488 resolved_versions: HashMap::new(),
489 parsed_at: Instant::now(),
490 loading_state: LoadingState::Idle,
491 loading_started_at: None,
492 }
493 }
494
495 pub fn new_without_parse_result(ecosystem_id: &'static str, content: String) -> Self {
500 let ecosystem = match ecosystem_id {
501 "cargo" => Ecosystem::Cargo,
502 "npm" => Ecosystem::Npm,
503 "pypi" => Ecosystem::Pypi,
504 "go" => Ecosystem::Go,
505 _ => Ecosystem::Cargo, };
507
508 Self {
509 ecosystem,
510 ecosystem_id,
511 content,
512 dependencies: vec![],
513 parse_result: None,
514 versions: HashMap::new(),
515 cached_versions: HashMap::new(),
516 resolved_versions: HashMap::new(),
517 parsed_at: Instant::now(),
518 loading_state: LoadingState::Idle,
519 loading_started_at: None,
520 }
521 }
522
523 pub fn parse_result(&self) -> Option<&dyn ParseResult> {
525 self.parse_result.as_ref().map(std::convert::AsRef::as_ref)
526 }
527
528 pub fn update_versions(&mut self, versions: HashMap<String, UnifiedVersion>) {
530 self.versions = versions;
531 }
532
533 pub fn update_cached_versions(&mut self, versions: HashMap<String, String>) {
535 self.cached_versions = versions;
536 }
537
538 pub fn update_resolved_versions(&mut self, versions: HashMap<String, String>) {
540 self.resolved_versions = versions;
541 }
542
543 pub fn set_loading(&mut self) {
561 self.loading_state = LoadingState::Loading;
562 self.loading_started_at = Some(Instant::now());
563 }
564
565 pub fn set_loaded(&mut self) {
579 self.loading_state = LoadingState::Loaded;
580 self.loading_started_at = None;
581 }
582
583 pub fn set_failed(&mut self) {
597 self.loading_state = LoadingState::Failed;
598 self.loading_started_at = None;
599 }
600
601 #[must_use]
618 pub fn loading_duration(&self) -> Option<Duration> {
619 self.loading_started_at
620 .map(|start| Instant::now().duration_since(start))
621 }
622}
623
624pub struct ServerState {
641 pub documents: DashMap<Uri, DocumentState>,
643 pub cache: Arc<HttpCache>,
645 pub lockfile_cache: Arc<LockFileCache>,
647 pub ecosystem_registry: Arc<EcosystemRegistry>,
649 pub cold_start_limiter: ColdStartLimiter,
651 tasks: tokio::sync::RwLock<HashMap<Uri, JoinHandle<()>>>,
653}
654
655impl ServerState {
656 pub fn new() -> Self {
658 let cache = Arc::new(HttpCache::new());
659 let lockfile_cache = Arc::new(LockFileCache::new());
660 let ecosystem_registry = Arc::new(EcosystemRegistry::new());
661
662 crate::register_ecosystems(&ecosystem_registry, Arc::clone(&cache));
664
665 let cold_start_limiter = ColdStartLimiter::new(Duration::from_millis(100));
667
668 Self {
669 documents: DashMap::new(),
670 cache,
671 lockfile_cache,
672 ecosystem_registry,
673 cold_start_limiter,
674 tasks: tokio::sync::RwLock::new(HashMap::new()),
675 }
676 }
677
678 pub fn get_document(
684 &self,
685 uri: &Uri,
686 ) -> Option<dashmap::mapref::one::Ref<'_, Uri, DocumentState>> {
687 self.documents.get(uri)
688 }
689
690 pub fn get_document_clone(&self, uri: &Uri) -> Option<DocumentState> {
720 self.documents.get(uri).map(|doc| doc.clone())
721 }
722
723 pub fn update_document(&self, uri: Uri, state: DocumentState) {
728 self.documents.insert(uri, state);
729 }
730
731 pub fn remove_document(&self, uri: &Uri) -> Option<(Uri, DocumentState)> {
735 self.documents.remove(uri)
736 }
737
738 pub async fn spawn_background_task(&self, uri: Uri, task: JoinHandle<()>) {
747 let mut tasks = self.tasks.write().await;
748
749 if let Some(old_task) = tasks.remove(&uri) {
751 old_task.abort();
752 }
753
754 tasks.insert(uri, task);
755 }
756
757 pub async fn cancel_background_task(&self, uri: &Uri) {
761 let mut tasks = self.tasks.write().await;
762 if let Some(task) = tasks.remove(uri) {
763 task.abort();
764 }
765 }
766
767 pub fn document_count(&self) -> usize {
769 self.documents.len()
770 }
771}
772
773impl Default for ServerState {
774 fn default() -> Self {
775 Self::new()
776 }
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782
783 mod loading_state_tests {
792 use super::*;
793
794 #[test]
795 fn test_loading_state_default() {
796 let state = LoadingState::default();
797 assert_eq!(state, LoadingState::Idle);
798 }
799
800 #[test]
801 fn test_loading_state_transitions() {
802 use std::time::Duration;
803
804 let content = "[dependencies]\nserde = \"1.0\"".to_string();
805 let mut doc = DocumentState::new_without_parse_result("cargo", content);
806
807 assert_eq!(doc.loading_state, LoadingState::Idle);
809 assert!(doc.loading_started_at.is_none());
810
811 doc.set_loading();
813 assert_eq!(doc.loading_state, LoadingState::Loading);
814 assert!(doc.loading_started_at.is_some());
815
816 std::thread::sleep(Duration::from_millis(10));
818
819 let duration = doc.loading_duration();
821 assert!(duration.is_some());
822 assert!(duration.unwrap() >= Duration::from_millis(10));
823
824 doc.set_loaded();
826 assert_eq!(doc.loading_state, LoadingState::Loaded);
827 assert!(doc.loading_started_at.is_none());
828 assert!(doc.loading_duration().is_none());
829 }
830
831 #[test]
832 fn test_loading_state_failed_transition() {
833 let content = "[dependencies]\nserde = \"1.0\"".to_string();
834 let mut doc = DocumentState::new_without_parse_result("cargo", content);
835
836 doc.set_loading();
837 assert_eq!(doc.loading_state, LoadingState::Loading);
838
839 doc.set_failed();
840 assert_eq!(doc.loading_state, LoadingState::Failed);
841 assert!(doc.loading_started_at.is_none());
842 }
843
844 #[test]
845 fn test_loading_state_clone() {
846 let content = "[dependencies]\nserde = \"1.0\"".to_string();
847 let mut doc = DocumentState::new_without_parse_result("cargo", content);
848
849 doc.set_loading();
850 let cloned = doc.clone();
851
852 assert_eq!(cloned.loading_state, LoadingState::Loading);
853 assert!(cloned.loading_started_at.is_some());
854 }
855
856 #[test]
857 fn test_loading_state_debug() {
858 let content = "[dependencies]\nserde = \"1.0\"".to_string();
859 let mut doc = DocumentState::new_without_parse_result("cargo", content);
860 doc.set_loading();
861
862 let debug_str = format!("{:?}", doc);
863 assert!(debug_str.contains("loading_state"));
864 assert!(debug_str.contains("Loading"));
865 }
866
867 #[test]
868 fn test_loading_duration_none_when_idle() {
869 let content = "[dependencies]\nserde = \"1.0\"".to_string();
870 let doc = DocumentState::new_without_parse_result("cargo", content);
871
872 assert_eq!(doc.loading_state, LoadingState::Idle);
873 assert!(doc.loading_duration().is_none());
874 }
875
876 #[test]
877 fn test_loading_state_equality() {
878 assert_eq!(LoadingState::Idle, LoadingState::Idle);
879 assert_eq!(LoadingState::Loading, LoadingState::Loading);
880 assert_eq!(LoadingState::Loaded, LoadingState::Loaded);
881 assert_eq!(LoadingState::Failed, LoadingState::Failed);
882
883 assert_ne!(LoadingState::Idle, LoadingState::Loading);
884 assert_ne!(LoadingState::Loading, LoadingState::Loaded);
885 }
886
887 #[test]
888 fn test_loading_duration_tracks_time_correctly() {
889 use std::time::Duration;
890
891 let content = "[dependencies]\nserde = \"1.0\"".to_string();
892 let mut doc = DocumentState::new_without_parse_result("cargo", content);
893
894 doc.set_loading();
895
896 let duration1 = doc.loading_duration().unwrap();
898 std::thread::sleep(Duration::from_millis(20));
899 let duration2 = doc.loading_duration().unwrap();
900
901 assert!(duration2 > duration1, "Duration should increase over time");
902 }
903
904 #[tokio::test]
905 async fn test_concurrent_loading_state_mutations() {
906 use std::sync::Arc;
907 use tokio::sync::Barrier;
908
909 let state = Arc::new(ServerState::new());
910 let uri = Uri::from_file_path("/concurrent-loading-test.toml").unwrap();
911
912 let doc = DocumentState::new_without_parse_result("cargo", String::new());
913 state.update_document(uri.clone(), doc);
914
915 let barrier = Arc::new(Barrier::new(10));
916 let mut handles = vec![];
917
918 for i in 0..10 {
919 let state_clone = Arc::clone(&state);
920 let uri_clone = uri.clone();
921 let barrier_clone = Arc::clone(&barrier);
922
923 handles.push(tokio::spawn(async move {
924 barrier_clone.wait().await;
925 if let Some(mut doc) = state_clone.documents.get_mut(&uri_clone) {
926 if i % 3 == 0 {
927 doc.set_loading();
928 } else if i % 3 == 1 {
929 doc.set_loaded();
930 } else {
931 doc.set_failed();
932 }
933 }
934 }));
935 }
936
937 for handle in handles {
938 handle.await.unwrap();
939 }
940
941 let doc = state.get_document(&uri).unwrap();
942 assert!(matches!(
943 doc.loading_state,
944 LoadingState::Idle
945 | LoadingState::Loading
946 | LoadingState::Loaded
947 | LoadingState::Failed
948 ));
949 }
950
951 #[test]
952 fn test_set_loaded_idempotent() {
953 let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
954
955 doc.set_loading();
956 doc.set_loaded();
957
958 doc.set_loaded();
960
961 assert_eq!(doc.loading_state, LoadingState::Loaded);
962 assert!(doc.loading_started_at.is_none());
963 }
964
965 #[test]
966 fn test_set_loading_resets_timer() {
967 let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
968
969 doc.set_loading();
970 let first_start = doc.loading_started_at.unwrap();
971
972 std::thread::sleep(std::time::Duration::from_millis(10));
973
974 doc.set_loading();
976 let second_start = doc.loading_started_at.unwrap();
977
978 assert!(second_start > first_start, "Timer should be reset");
979 assert_eq!(doc.loading_state, LoadingState::Loading);
980 }
981
982 #[test]
983 fn test_retry_after_failure() {
984 let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
985
986 doc.set_loading();
987 doc.set_failed();
988 assert_eq!(doc.loading_state, LoadingState::Failed);
989 assert!(doc.loading_started_at.is_none());
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 #[test]
1001 fn test_refresh_after_loaded() {
1002 let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
1003
1004 doc.set_loading();
1005 doc.set_loaded();
1006 assert_eq!(doc.loading_state, LoadingState::Loaded);
1007
1008 doc.set_loading();
1010 assert_eq!(doc.loading_state, LoadingState::Loading);
1011 assert!(doc.loading_started_at.is_some());
1012
1013 doc.set_loaded();
1014 assert_eq!(doc.loading_state, LoadingState::Loaded);
1015 }
1016 }
1017
1018 #[test]
1019 fn test_ecosystem_from_filename() {
1020 #[cfg(feature = "cargo")]
1021 assert_eq!(
1022 Ecosystem::from_filename("Cargo.toml"),
1023 Some(Ecosystem::Cargo)
1024 );
1025 #[cfg(feature = "npm")]
1026 assert_eq!(
1027 Ecosystem::from_filename("package.json"),
1028 Some(Ecosystem::Npm)
1029 );
1030 #[cfg(feature = "pypi")]
1031 assert_eq!(
1032 Ecosystem::from_filename("pyproject.toml"),
1033 Some(Ecosystem::Pypi)
1034 );
1035 #[cfg(feature = "go")]
1036 assert_eq!(Ecosystem::from_filename("go.mod"), Some(Ecosystem::Go));
1037 assert_eq!(Ecosystem::from_filename("unknown.txt"), None);
1038 }
1039
1040 #[test]
1041 fn test_ecosystem_from_uri() {
1042 #[cfg(feature = "cargo")]
1043 {
1044 let cargo_uri = Uri::from_file_path("/path/to/Cargo.toml").unwrap();
1045 assert_eq!(Ecosystem::from_uri(&cargo_uri), Some(Ecosystem::Cargo));
1046 }
1047 #[cfg(feature = "npm")]
1048 {
1049 let npm_uri = Uri::from_file_path("/path/to/package.json").unwrap();
1050 assert_eq!(Ecosystem::from_uri(&npm_uri), Some(Ecosystem::Npm));
1051 }
1052 #[cfg(feature = "pypi")]
1053 {
1054 let pypi_uri = Uri::from_file_path("/path/to/pyproject.toml").unwrap();
1055 assert_eq!(Ecosystem::from_uri(&pypi_uri), Some(Ecosystem::Pypi));
1056 }
1057 #[cfg(feature = "go")]
1058 {
1059 let go_uri = Uri::from_file_path("/path/to/go.mod").unwrap();
1060 assert_eq!(Ecosystem::from_uri(&go_uri), Some(Ecosystem::Go));
1061 }
1062 let unknown_uri = Uri::from_file_path("/path/to/README.md").unwrap();
1063 assert_eq!(Ecosystem::from_uri(&unknown_uri), None);
1064 }
1065
1066 #[test]
1067 fn test_ecosystem_from_filename_edge_cases() {
1068 assert_eq!(Ecosystem::from_filename(""), None);
1069 assert_eq!(Ecosystem::from_filename("cargo.toml"), None);
1070 assert_eq!(Ecosystem::from_filename("CARGO.TOML"), None);
1071 assert_eq!(Ecosystem::from_filename("requirements.txt"), None);
1072 }
1073
1074 #[test]
1075 fn test_server_state_creation() {
1076 let state = ServerState::new();
1077 assert_eq!(state.document_count(), 0);
1078 assert!(state.cache.is_empty(), "Cache should start empty");
1079 }
1080
1081 #[test]
1082 fn test_server_state_default() {
1083 let state = ServerState::default();
1084 assert_eq!(state.document_count(), 0);
1085 }
1086
1087 #[tokio::test]
1088 async fn test_server_state_background_tasks() {
1089 let state = ServerState::new();
1090 let uri = Uri::from_file_path("/test.toml").unwrap();
1091
1092 let task = tokio::spawn(async {
1093 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1094 });
1095
1096 state.spawn_background_task(uri.clone(), task).await;
1097 state.cancel_background_task(&uri).await;
1098 }
1099
1100 #[tokio::test]
1101 async fn test_spawn_background_task_cancels_previous() {
1102 let state = ServerState::new();
1103 let uri = Uri::from_file_path("/test.toml").unwrap();
1104
1105 let task1 = tokio::spawn(async {
1106 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1107 });
1108 state.spawn_background_task(uri.clone(), task1).await;
1109
1110 let task2 = tokio::spawn(async {
1111 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1112 });
1113 state.spawn_background_task(uri.clone(), task2).await;
1114 state.cancel_background_task(&uri).await;
1115 }
1116
1117 #[tokio::test]
1118 async fn test_cancel_background_task_nonexistent() {
1119 let state = ServerState::new();
1120 let uri = Uri::from_file_path("/test.toml").unwrap();
1121 state.cancel_background_task(&uri).await;
1122 }
1123
1124 mod cold_start_limiter {
1129 use super::*;
1130 use std::time::Duration;
1131
1132 #[test]
1133 fn test_allows_first_request() {
1134 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1135 let uri = Uri::from_file_path("/test.toml").unwrap();
1136 assert!(
1137 limiter.allow_cold_start(&uri),
1138 "First request should be allowed"
1139 );
1140 }
1141
1142 #[test]
1143 fn test_blocks_rapid_requests() {
1144 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1145 let uri = Uri::from_file_path("/test.toml").unwrap();
1146
1147 assert!(limiter.allow_cold_start(&uri), "First request allowed");
1148 assert!(
1149 !limiter.allow_cold_start(&uri),
1150 "Second immediate request should be blocked"
1151 );
1152 }
1153
1154 #[tokio::test]
1155 async fn test_allows_after_interval() {
1156 let limiter = ColdStartLimiter::new(Duration::from_millis(50));
1157 let uri = Uri::from_file_path("/test.toml").unwrap();
1158
1159 assert!(limiter.allow_cold_start(&uri), "First request allowed");
1160 tokio::time::sleep(Duration::from_millis(60)).await;
1161 assert!(
1162 limiter.allow_cold_start(&uri),
1163 "Request after interval should be allowed"
1164 );
1165 }
1166
1167 #[test]
1168 fn test_different_uris_independent() {
1169 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1170 let uri1 = Uri::from_file_path("/test1.toml").unwrap();
1171 let uri2 = Uri::from_file_path("/test2.toml").unwrap();
1172
1173 assert!(limiter.allow_cold_start(&uri1), "URI 1 first request");
1174 assert!(limiter.allow_cold_start(&uri2), "URI 2 first request");
1175 assert!(
1176 !limiter.allow_cold_start(&uri1),
1177 "URI 1 second request blocked"
1178 );
1179 assert!(
1180 !limiter.allow_cold_start(&uri2),
1181 "URI 2 second request blocked"
1182 );
1183 }
1184
1185 #[test]
1186 fn test_cleanup() {
1187 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1188 let uri1 = Uri::from_file_path("/test1.toml").unwrap();
1189 let uri2 = Uri::from_file_path("/test2.toml").unwrap();
1190
1191 limiter.allow_cold_start(&uri1);
1192 limiter.allow_cold_start(&uri2);
1193 assert_eq!(limiter.tracked_count(), 2, "Should track 2 URIs");
1194
1195 limiter.cleanup_old_entries(Duration::from_millis(0));
1196 assert_eq!(
1197 limiter.tracked_count(),
1198 0,
1199 "All entries should be cleaned up"
1200 );
1201 }
1202
1203 #[tokio::test]
1204 async fn test_concurrent_access() {
1205 use std::sync::Arc;
1206
1207 let limiter = Arc::new(ColdStartLimiter::new(Duration::from_millis(100)));
1208 let uri = Uri::from_file_path("/concurrent-test.toml").unwrap();
1209
1210 let mut handles = vec![];
1211 const CONCURRENT_TASKS: usize = 10;
1212
1213 for _ in 0..CONCURRENT_TASKS {
1214 let limiter_clone = Arc::clone(&limiter);
1215 let uri_clone = uri.clone();
1216 let handle =
1217 tokio::spawn(async move { limiter_clone.allow_cold_start(&uri_clone) });
1218 handles.push(handle);
1219 }
1220
1221 let mut results = vec![];
1222 for handle in handles {
1223 results.push(handle.await.unwrap());
1224 }
1225
1226 let allowed_count = results.iter().filter(|&&allowed| allowed).count();
1227 assert_eq!(allowed_count, 1, "Exactly one concurrent request allowed");
1228
1229 let blocked_count = results.iter().filter(|&&allowed| !allowed).count();
1230 assert_eq!(
1231 blocked_count,
1232 CONCURRENT_TASKS - 1,
1233 "Rest should be blocked"
1234 );
1235 }
1236 }
1237
1238 #[cfg(feature = "cargo")]
1243 mod cargo_tests {
1244 use super::*;
1245 use deps_cargo::{DependencySection, DependencySource};
1246 use tower_lsp_server::ls_types::{Position, Range};
1247
1248 fn create_test_dependency() -> UnifiedDependency {
1249 UnifiedDependency::Cargo(ParsedDependency {
1250 name: "serde".into(),
1251 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1252 version_req: Some("1.0".into()),
1253 version_range: Some(Range::new(Position::new(0, 9), Position::new(0, 14))),
1254 features: vec![],
1255 features_range: None,
1256 source: DependencySource::Registry,
1257 workspace_inherited: false,
1258 section: DependencySection::Dependencies,
1259 })
1260 }
1261
1262 #[test]
1263 fn test_document_state_creation() {
1264 let deps = vec![create_test_dependency()];
1265 let state = DocumentState::new(Ecosystem::Cargo, "test content".into(), deps);
1266
1267 assert_eq!(state.ecosystem, Ecosystem::Cargo);
1268 assert_eq!(state.content, "test content");
1269 assert_eq!(state.dependencies.len(), 1);
1270 assert!(state.versions.is_empty());
1271 }
1272
1273 #[test]
1274 fn test_document_state_update_versions() {
1275 let deps = vec![create_test_dependency()];
1276 let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1277
1278 let mut versions = HashMap::new();
1279 versions.insert(
1280 "serde".into(),
1281 UnifiedVersion::Cargo(CargoVersion {
1282 num: "1.0.0".into(),
1283 yanked: false,
1284 features: HashMap::new(),
1285 }),
1286 );
1287
1288 state.update_versions(versions);
1289 assert_eq!(state.versions.len(), 1);
1290 assert!(state.versions.contains_key("serde"));
1291 }
1292
1293 #[test]
1294 fn test_server_state_document_operations() {
1295 let state = ServerState::new();
1296 let uri = Uri::from_file_path("/test.toml").unwrap();
1297 let deps = vec![create_test_dependency()];
1298 let doc_state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1299
1300 state.update_document(uri.clone(), doc_state);
1301 assert_eq!(state.document_count(), 1);
1302
1303 let retrieved = state.get_document(&uri);
1304 assert!(retrieved.is_some());
1305 assert_eq!(retrieved.unwrap().content, "test");
1306
1307 let removed = state.remove_document(&uri);
1308 assert!(removed.is_some());
1309 assert_eq!(state.document_count(), 0);
1310 }
1311
1312 #[test]
1313 fn test_unified_dependency_name() {
1314 let cargo_dep = create_test_dependency();
1315 assert_eq!(cargo_dep.name(), "serde");
1316 assert_eq!(cargo_dep.version_req(), Some("1.0"));
1317 assert!(cargo_dep.is_registry());
1318 }
1319
1320 #[test]
1321 fn test_unified_dependency_git_source() {
1322 let git_dep = UnifiedDependency::Cargo(ParsedDependency {
1323 name: "custom".into(),
1324 name_range: Range::new(Position::new(0, 0), Position::new(0, 6)),
1325 version_req: None,
1326 version_range: None,
1327 features: vec![],
1328 features_range: None,
1329 source: DependencySource::Git {
1330 url: "https://github.com/user/repo".into(),
1331 rev: None,
1332 },
1333 workspace_inherited: false,
1334 section: DependencySection::Dependencies,
1335 });
1336 assert!(!git_dep.is_registry());
1337 }
1338
1339 #[test]
1340 fn test_unified_version() {
1341 let version = UnifiedVersion::Cargo(CargoVersion {
1342 num: "1.0.0".into(),
1343 yanked: false,
1344 features: HashMap::new(),
1345 });
1346 assert_eq!(version.version_string(), "1.0.0");
1347 assert!(!version.is_yanked());
1348 }
1349
1350 #[test]
1351 fn test_document_state_new_from_parse_result() {
1352 let state = ServerState::new();
1353 let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
1354 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
1355 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1356
1357 let parse_result = tokio::runtime::Runtime::new()
1358 .unwrap()
1359 .block_on(ecosystem.parse_manifest(&content, &uri))
1360 .unwrap();
1361
1362 let doc_state =
1363 DocumentState::new_from_parse_result("cargo", content.clone(), parse_result);
1364
1365 assert_eq!(doc_state.ecosystem_id, "cargo");
1366 assert_eq!(doc_state.content, content);
1367 assert!(doc_state.parse_result.is_some());
1368 }
1369
1370 #[test]
1371 fn test_document_state_new_without_parse_result() {
1372 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1373 let doc_state = DocumentState::new_without_parse_result("cargo", content);
1374
1375 assert_eq!(doc_state.ecosystem_id, "cargo");
1376 assert_eq!(doc_state.ecosystem, Ecosystem::Cargo);
1377 assert!(doc_state.parse_result.is_none());
1378 assert!(doc_state.dependencies.is_empty());
1379 }
1380
1381 #[test]
1382 fn test_document_state_update_resolved_versions() {
1383 let deps = vec![create_test_dependency()];
1384 let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1385
1386 let mut resolved = HashMap::new();
1387 resolved.insert("serde".into(), "1.0.195".into());
1388
1389 state.update_resolved_versions(resolved);
1390 assert_eq!(state.resolved_versions.len(), 1);
1391 assert_eq!(
1392 state.resolved_versions.get("serde"),
1393 Some(&"1.0.195".into())
1394 );
1395 }
1396
1397 #[test]
1398 fn test_document_state_update_cached_versions() {
1399 let deps = vec![create_test_dependency()];
1400 let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1401
1402 let mut cached = HashMap::new();
1403 cached.insert("serde".into(), "1.0.210".into());
1404
1405 state.update_cached_versions(cached);
1406 assert_eq!(state.cached_versions.len(), 1);
1407 }
1408
1409 #[test]
1410 fn test_document_state_parse_result_accessor() {
1411 let deps = vec![create_test_dependency()];
1412 let state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1413 assert!(state.parse_result().is_none());
1414 }
1415
1416 #[test]
1417 fn test_document_state_clone() {
1418 let deps = vec![create_test_dependency()];
1419 let state = DocumentState::new(Ecosystem::Cargo, "test content".into(), deps);
1420 let cloned = state.clone();
1421
1422 assert_eq!(cloned.ecosystem, state.ecosystem);
1423 assert_eq!(cloned.content, state.content);
1424 assert_eq!(cloned.dependencies.len(), state.dependencies.len());
1425 assert!(cloned.parse_result.is_none());
1426 }
1427
1428 #[test]
1429 fn test_document_state_debug() {
1430 let deps = vec![create_test_dependency()];
1431 let state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1432 let debug_str = format!("{state:?}");
1433 assert!(debug_str.contains("DocumentState"));
1434 }
1435 }
1436
1437 #[cfg(feature = "npm")]
1442 mod npm_tests {
1443 use super::*;
1444 use deps_npm::{NpmDependency, NpmDependencySection};
1445 use tower_lsp_server::ls_types::{Position, Range};
1446
1447 #[test]
1448 fn test_unified_dependency() {
1449 let npm_dep = UnifiedDependency::Npm(NpmDependency {
1450 name: "express".into(),
1451 name_range: Range::new(Position::new(0, 0), Position::new(0, 7)),
1452 version_req: Some("^4.0.0".into()),
1453 version_range: Some(Range::new(Position::new(0, 11), Position::new(0, 18))),
1454 section: NpmDependencySection::Dependencies,
1455 });
1456
1457 assert_eq!(npm_dep.name(), "express");
1458 assert_eq!(npm_dep.version_req(), Some("^4.0.0"));
1459 assert!(npm_dep.is_registry());
1460 }
1461
1462 #[test]
1463 fn test_unified_version() {
1464 let version = UnifiedVersion::Npm(deps_npm::NpmVersion {
1465 version: "4.18.2".into(),
1466 deprecated: false,
1467 });
1468 assert_eq!(version.version_string(), "4.18.2");
1469 assert!(!version.is_yanked());
1470 }
1471
1472 #[test]
1473 fn test_document_state_new_without_parse_result() {
1474 let content = r#"{"dependencies": {"express": "^4.18.0"}}"#.to_string();
1475 let doc_state = DocumentState::new_without_parse_result("npm", content);
1476
1477 assert_eq!(doc_state.ecosystem_id, "npm");
1478 assert_eq!(doc_state.ecosystem, Ecosystem::Npm);
1479 assert!(doc_state.parse_result.is_none());
1480 }
1481 }
1482
1483 #[cfg(feature = "pypi")]
1488 mod pypi_tests {
1489 use super::*;
1490 use deps_pypi::{PypiDependency, PypiDependencySection, PypiDependencySource};
1491 use tower_lsp_server::ls_types::{Position, Range};
1492
1493 #[test]
1494 fn test_unified_dependency() {
1495 let pypi_dep = UnifiedDependency::Pypi(PypiDependency {
1496 name: "requests".into(),
1497 name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
1498 version_req: Some(">=2.0.0".into()),
1499 version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 18))),
1500 extras: vec![],
1501 extras_range: None,
1502 markers: None,
1503 markers_range: None,
1504 source: PypiDependencySource::PyPI,
1505 section: PypiDependencySection::Dependencies,
1506 });
1507
1508 assert_eq!(pypi_dep.name(), "requests");
1509 assert_eq!(pypi_dep.version_req(), Some(">=2.0.0"));
1510 assert!(pypi_dep.is_registry());
1511 }
1512
1513 #[test]
1514 fn test_unified_version() {
1515 let version = UnifiedVersion::Pypi(deps_pypi::PypiVersion {
1516 version: "2.31.0".into(),
1517 yanked: true,
1518 });
1519 assert_eq!(version.version_string(), "2.31.0");
1520 assert!(version.is_yanked());
1521 }
1522
1523 #[test]
1524 fn test_document_state_new_without_parse_result() {
1525 let content = "[project]\ndependencies = [\"requests>=2.0.0\"]\n".to_string();
1526 let doc_state = DocumentState::new_without_parse_result("pypi", content);
1527
1528 assert_eq!(doc_state.ecosystem_id, "pypi");
1529 assert_eq!(doc_state.ecosystem, Ecosystem::Pypi);
1530 assert!(doc_state.parse_result.is_none());
1531 }
1532 }
1533
1534 #[cfg(feature = "go")]
1539 mod go_tests {
1540 use super::*;
1541 use deps_go::{GoDependency, GoDirective, GoVersion};
1542 use tower_lsp_server::ls_types::{Position, Range};
1543
1544 fn create_test_dependency() -> UnifiedDependency {
1545 UnifiedDependency::Go(GoDependency {
1546 module_path: "github.com/gin-gonic/gin".into(),
1547 module_path_range: Range::new(Position::new(0, 0), Position::new(0, 25)),
1548 version: Some("v1.9.1".into()),
1549 version_range: Some(Range::new(Position::new(0, 26), Position::new(0, 32))),
1550 directive: GoDirective::Require,
1551 indirect: false,
1552 })
1553 }
1554
1555 #[test]
1556 fn test_unified_dependency() {
1557 let go_dep = create_test_dependency();
1558 assert_eq!(go_dep.name(), "github.com/gin-gonic/gin");
1559 assert_eq!(go_dep.version_req(), Some("v1.9.1"));
1560 assert!(go_dep.is_registry());
1561 }
1562
1563 #[test]
1564 fn test_unified_dependency_name_range() {
1565 let range = Range::new(Position::new(5, 10), Position::new(5, 35));
1566 let go_dep = UnifiedDependency::Go(GoDependency {
1567 module_path: "github.com/example/pkg".into(),
1568 module_path_range: range,
1569 version: Some("v1.0.0".into()),
1570 version_range: Some(Range::new(Position::new(5, 36), Position::new(5, 42))),
1571 directive: GoDirective::Require,
1572 indirect: false,
1573 });
1574 assert_eq!(go_dep.name_range(), range);
1575 }
1576
1577 #[test]
1578 fn test_unified_dependency_version_range() {
1579 let version_range = Range::new(Position::new(5, 36), Position::new(5, 42));
1580 let go_dep = UnifiedDependency::Go(GoDependency {
1581 module_path: "github.com/example/pkg".into(),
1582 module_path_range: Range::new(Position::new(5, 10), Position::new(5, 35)),
1583 version: Some("v1.0.0".into()),
1584 version_range: Some(version_range),
1585 directive: GoDirective::Require,
1586 indirect: false,
1587 });
1588 assert_eq!(go_dep.version_range(), Some(version_range));
1589 }
1590
1591 #[test]
1592 fn test_unified_dependency_no_version() {
1593 let go_dep = UnifiedDependency::Go(GoDependency {
1594 module_path: "github.com/example/pkg".into(),
1595 module_path_range: Range::new(Position::new(5, 10), Position::new(5, 35)),
1596 version: None,
1597 version_range: None,
1598 directive: GoDirective::Require,
1599 indirect: false,
1600 });
1601 assert_eq!(go_dep.version_req(), None);
1602 assert_eq!(go_dep.version_range(), None);
1603 }
1604
1605 #[test]
1606 fn test_unified_version() {
1607 let version = UnifiedVersion::Go(GoVersion {
1608 version: "v1.9.1".into(),
1609 time: Some("2023-07-18T14:30:00Z".into()),
1610 is_pseudo: false,
1611 retracted: false,
1612 });
1613 assert_eq!(version.version_string(), "v1.9.1");
1614 assert!(!version.is_yanked());
1615 }
1616
1617 #[test]
1618 fn test_unified_version_retracted() {
1619 let version = UnifiedVersion::Go(GoVersion {
1620 version: "v1.0.0".into(),
1621 time: None,
1622 is_pseudo: false,
1623 retracted: true,
1624 });
1625 assert_eq!(version.version_string(), "v1.0.0");
1626 assert!(version.is_yanked());
1627 }
1628
1629 #[test]
1630 fn test_unified_version_pseudo() {
1631 let version = UnifiedVersion::Go(GoVersion {
1632 version: "v0.0.0-20191109021931-daa7c04131f5".into(),
1633 time: Some("2019-11-09T02:19:31Z".into()),
1634 is_pseudo: true,
1635 retracted: false,
1636 });
1637 assert_eq!(
1638 version.version_string(),
1639 "v0.0.0-20191109021931-daa7c04131f5"
1640 );
1641 assert!(!version.is_yanked());
1642 }
1643
1644 #[test]
1645 fn test_document_state_new() {
1646 let deps = vec![create_test_dependency()];
1647 let state = DocumentState::new(Ecosystem::Go, "test content".into(), deps);
1648
1649 assert_eq!(state.ecosystem, Ecosystem::Go);
1650 assert_eq!(state.ecosystem_id, "go");
1651 assert_eq!(state.dependencies.len(), 1);
1652 }
1653
1654 #[test]
1655 fn test_document_state_new_without_parse_result() {
1656 let content =
1657 "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1658 .to_string();
1659 let doc_state = DocumentState::new_without_parse_result("go", content);
1660
1661 assert_eq!(doc_state.ecosystem_id, "go");
1662 assert_eq!(doc_state.ecosystem, Ecosystem::Go);
1663 assert!(doc_state.parse_result.is_none());
1664 }
1665
1666 #[test]
1667 fn test_document_state_new_from_parse_result() {
1668 let state = ServerState::new();
1669 let uri = Uri::from_file_path("/test/go.mod").unwrap();
1670 let ecosystem = state.ecosystem_registry.get("go").unwrap();
1671 let content =
1672 "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1673 .to_string();
1674
1675 let parse_result = tokio::runtime::Runtime::new()
1676 .unwrap()
1677 .block_on(ecosystem.parse_manifest(&content, &uri))
1678 .unwrap();
1679
1680 let doc_state =
1681 DocumentState::new_from_parse_result("go", content.clone(), parse_result);
1682
1683 assert_eq!(doc_state.ecosystem_id, "go");
1684 assert!(doc_state.parse_result.is_some());
1685 }
1686 }
1687}