1use dashmap::DashMap;
2use deps_core::HttpCache;
3use deps_core::lockfile::LockFileCache;
4use deps_core::net_policy::RegistryAccessPolicy;
5use deps_core::osv::{OsvClient, VulnerabilityMap};
6use deps_core::{
7 ConcreteVersion, DependencyOutcomes, DepsDevClient, EcosystemId, EcosystemRegistry,
8 PackageName, PackageVersions, ParseResult,
9};
10use std::collections::HashMap;
11use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
12use std::sync::{Arc, RwLock};
13use std::time::{Duration, Instant};
14use tokio::task::JoinHandle;
15use tower_lsp_server::Client;
16use tower_lsp_server::ls_types::Uri;
17
18pub(crate) const CLIENT_REFRESH_TIMEOUT: Duration = Duration::from_secs(5);
31
32const FETCH_PERMITS: usize = 4;
43
44pub use deps_core::LoadingState;
46
47pub struct DocumentState {
69 pub ecosystem: EcosystemId,
71 pub content: String,
73 parse_result: Option<Arc<dyn ParseResult>>,
78 pub cached_versions: HashMap<PackageName, PackageVersions>,
81 pub resolved_versions: HashMap<PackageName, ConcreteVersion>,
83 pub vulnerabilities: VulnerabilityMap,
87 pub outcomes: DependencyOutcomes,
96 pub parsed_at: Instant,
98 pub loading_state: LoadingState,
100 pub loading_started_at: Option<Instant>,
102 pub version: Option<i32>,
109}
110
111impl Clone for DocumentState {
112 fn clone(&self) -> Self {
113 Self {
114 ecosystem: self.ecosystem,
115 content: self.content.clone(),
116 parse_result: self.parse_result.clone(),
118 cached_versions: self.cached_versions.clone(),
119 resolved_versions: self.resolved_versions.clone(),
120 vulnerabilities: self.vulnerabilities.clone(),
121 outcomes: self.outcomes.clone(),
122 parsed_at: self.parsed_at,
123 loading_state: self.loading_state,
124 loading_started_at: self.loading_started_at,
126 version: self.version,
127 }
128 }
129}
130
131#[derive(Debug)]
151pub struct ColdStartLimiter {
152 last_attempts: DashMap<Uri, Instant>,
154 min_interval_ms: AtomicU64,
159}
160
161impl ColdStartLimiter {
162 pub fn new(min_interval: Duration) -> Self {
164 Self {
165 last_attempts: DashMap::new(),
166 min_interval_ms: AtomicU64::new(min_interval.as_millis() as u64),
167 }
168 }
169
170 pub fn set_min_interval(&self, min_interval: Duration) {
175 self.min_interval_ms
176 .store(min_interval.as_millis() as u64, Ordering::Relaxed);
177 }
178
179 pub fn allow_cold_start(&self, uri: &Uri) -> bool {
183 let min_interval = Duration::from_millis(self.min_interval_ms.load(Ordering::Relaxed));
184 let now = Instant::now();
185
186 if let Some(mut entry) = self.last_attempts.get_mut(uri) {
188 let elapsed = now.duration_since(*entry);
189 if elapsed < min_interval {
190 let retry_after = min_interval.checked_sub(elapsed).unwrap();
191 tracing::warn!(
192 "Cold start rate limited for {:?} (retry after {:?})",
193 uri,
194 retry_after
195 );
196 return false;
197 }
198 *entry = now;
199 } else {
200 self.last_attempts.insert(uri.clone(), now);
201 }
202
203 true
204 }
205
206 pub fn cleanup_old_entries(&self, max_age: Duration) {
211 let now = Instant::now();
212 self.last_attempts
213 .retain(|_, instant| now.duration_since(*instant) < max_age);
214 }
215
216 #[cfg(test)]
218 pub fn tracked_count(&self) -> usize {
219 self.last_attempts.len()
220 }
221}
222
223impl std::fmt::Debug for DocumentState {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 f.debug_struct("DocumentState")
226 .field("ecosystem", &self.ecosystem)
227 .field("ecosystem_id", &self.ecosystem_id())
228 .field("content_len", &self.content.len())
229 .field("has_parse_result", &self.parse_result.is_some())
230 .field("cached_versions_count", &self.cached_versions.len())
231 .field("resolved_versions_count", &self.resolved_versions.len())
232 .field("vulnerabilities_count", &self.vulnerabilities.len())
233 .field("yanked_versions_count", &self.outcomes.yanked_count())
234 .field("deprecations_count", &self.outcomes.deprecation_count())
235 .field("fetch_failed_count", &self.outcomes.fetch_failure_count())
236 .field("parsed_at", &self.parsed_at)
237 .field("loading_state", &self.loading_state)
238 .field("loading_started_at", &self.loading_started_at)
239 .field("version", &self.version)
240 .finish()
241 }
242}
243
244impl DocumentState {
245 pub fn new_from_parse_result(
249 ecosystem: EcosystemId,
250 content: String,
251 parse_result: Box<dyn ParseResult>,
252 ) -> Self {
253 Self {
254 ecosystem,
255 content,
256 parse_result: Some(Arc::from(parse_result)),
257 cached_versions: HashMap::new(),
258 resolved_versions: HashMap::new(),
259 vulnerabilities: VulnerabilityMap::new(),
260 outcomes: DependencyOutcomes::new(),
261 parsed_at: Instant::now(),
262 loading_state: LoadingState::Idle,
263 loading_started_at: None,
264 version: None,
265 }
266 }
267
268 pub fn new_without_parse_result(ecosystem: EcosystemId, content: String) -> Self {
273 Self {
274 ecosystem,
275 content,
276 parse_result: None,
277 cached_versions: HashMap::new(),
278 resolved_versions: HashMap::new(),
279 vulnerabilities: VulnerabilityMap::new(),
280 outcomes: DependencyOutcomes::new(),
281 parsed_at: Instant::now(),
282 loading_state: LoadingState::Idle,
283 loading_started_at: None,
284 version: None,
285 }
286 }
287
288 pub fn ecosystem_id(&self) -> &'static str {
292 self.ecosystem.id()
293 }
294
295 pub fn parse_result(&self) -> Option<&dyn ParseResult> {
297 self.parse_result.as_deref()
298 }
299
300 pub fn parse_result_arc(&self) -> Option<Arc<dyn ParseResult>> {
307 self.parse_result.clone()
308 }
309
310 pub fn update_cached_versions(&mut self, versions: HashMap<PackageName, PackageVersions>) {
312 self.cached_versions = versions;
313 }
314
315 pub fn update_resolved_versions(&mut self, versions: HashMap<PackageName, ConcreteVersion>) {
317 self.resolved_versions = versions;
318 }
319
320 pub fn update_vulnerabilities(&mut self, vulnerabilities: VulnerabilityMap) {
322 self.vulnerabilities = vulnerabilities;
323 }
324
325 pub fn replace_outcomes(&mut self, outcomes: DependencyOutcomes) {
328 self.outcomes = outcomes;
329 }
330
331 pub fn set_version(&mut self, version: Option<i32>) {
346 self.version = version;
347 }
348
349 #[must_use]
374 pub fn is_ready_for_batch_update(&self) -> bool {
375 self.loading_state != LoadingState::Loading && self.version.is_some()
376 }
377
378 pub fn set_loading(&mut self) {
397 self.loading_state = LoadingState::Loading;
398 self.loading_started_at = Some(Instant::now());
399 }
400
401 pub fn set_loaded(&mut self) {
416 self.loading_state = LoadingState::Loaded;
417 self.loading_started_at = None;
418 }
419
420 pub fn set_failed(&mut self) {
435 self.loading_state = LoadingState::Failed;
436 self.loading_started_at = None;
437 }
438
439 #[must_use]
457 pub fn loading_duration(&self) -> Option<Duration> {
458 self.loading_started_at
459 .map(|start| Instant::now().duration_since(start))
460 }
461}
462
463pub struct ServerState {
480 pub documents: DashMap<Uri, DocumentState>,
482 pub cache: Arc<HttpCache>,
484 pub osv: Arc<OsvClient>,
487 pub deps_dev: Arc<DepsDevClient>,
493 pub lockfile_cache: Arc<LockFileCache>,
495 pub ecosystem_registry: Arc<EcosystemRegistry>,
497 pub registry_policy: Arc<RegistryAccessPolicy>,
504 pub nuget_user_profile_sources: Arc<AtomicBool>,
508 pub gitlab_instance_host: Arc<RwLock<Option<String>>>,
514 pub(crate) workspace_registry_ecosystems: Vec<&'static str>,
520 pub cold_start_limiter: ColdStartLimiter,
522 tasks: tokio::sync::RwLock<HashMap<Uri, JoinHandle<()>>>,
524 progress_supported: AtomicBool,
529 inlay_hint_refresh_supported: AtomicBool,
534 code_lens_refresh_supported: AtomicBool,
537 diagnostic_refresh_supported: AtomicBool,
543 pub(crate) fetch_permits: Arc<tokio::sync::Semaphore>,
546 pending_reparse: std::sync::Mutex<Option<PendingReparse>>,
552 config_generation: AtomicU64,
555}
556
557struct PendingReparse {
564 scope: crate::config::ReparseScope,
565 first_queued_at: Instant,
566}
567
568impl ServerState {
569 pub fn new() -> Self {
571 let registry_policy = Arc::new(RegistryAccessPolicy::default());
572 let cache = Arc::new(HttpCache::with_policy(Arc::clone(®istry_policy)));
577 let osv = Arc::new(OsvClient::new(Arc::clone(&cache)));
578 let deps_dev = Arc::new(DepsDevClient::new(Arc::clone(&cache)));
579 let lockfile_cache = Arc::new(LockFileCache::new());
580 let ecosystem_registry = Arc::new(EcosystemRegistry::new());
581 let nuget_user_profile_sources = Arc::new(AtomicBool::new(false));
582 let gitlab_instance_host = Arc::new(RwLock::new(None));
583
584 let workspace_registry_ecosystems = crate::register_ecosystems(
586 &ecosystem_registry,
587 Arc::clone(&cache),
588 &crate::EcosystemRuntime {
589 policy: Arc::clone(®istry_policy),
590 nuget_user_profile_sources: Arc::clone(&nuget_user_profile_sources),
591 gitlab_instance_host: Arc::clone(&gitlab_instance_host),
592 },
593 );
594
595 let cold_start_limiter = ColdStartLimiter::new(Duration::from_millis(
600 crate::config::ColdStartConfig::default().rate_limit_ms,
601 ));
602
603 Self {
604 documents: DashMap::new(),
605 cache,
606 osv,
607 deps_dev,
608 lockfile_cache,
609 ecosystem_registry,
610 registry_policy,
611 nuget_user_profile_sources,
612 gitlab_instance_host,
613 workspace_registry_ecosystems,
614 cold_start_limiter,
615 tasks: tokio::sync::RwLock::new(HashMap::new()),
616 progress_supported: AtomicBool::new(false),
617 inlay_hint_refresh_supported: AtomicBool::new(false),
618 code_lens_refresh_supported: AtomicBool::new(false),
619 diagnostic_refresh_supported: AtomicBool::new(false),
620 fetch_permits: Arc::new(tokio::sync::Semaphore::new(FETCH_PERMITS)),
621 pending_reparse: std::sync::Mutex::new(None),
622 config_generation: AtomicU64::new(0),
623 }
624 }
625
626 pub fn supports_progress(&self) -> bool {
628 self.progress_supported.load(Ordering::Relaxed)
629 }
630
631 pub fn set_progress_supported(&self, supported: bool) {
636 self.progress_supported.store(supported, Ordering::Relaxed);
637 }
638
639 pub fn inlay_hint_refresh_supported(&self) -> bool {
641 self.inlay_hint_refresh_supported.load(Ordering::Relaxed)
642 }
643
644 pub fn set_inlay_hint_refresh_supported(&self, supported: bool) {
649 self.inlay_hint_refresh_supported
650 .store(supported, Ordering::Relaxed);
651 }
652
653 pub fn code_lens_refresh_supported(&self) -> bool {
655 self.code_lens_refresh_supported.load(Ordering::Relaxed)
656 }
657
658 pub fn set_code_lens_refresh_supported(&self, supported: bool) {
663 self.code_lens_refresh_supported
664 .store(supported, Ordering::Relaxed);
665 }
666
667 pub fn diagnostic_refresh_supported(&self) -> bool {
669 self.diagnostic_refresh_supported.load(Ordering::Relaxed)
670 }
671
672 pub fn set_diagnostic_refresh_supported(&self, supported: bool) {
677 self.diagnostic_refresh_supported
678 .store(supported, Ordering::Relaxed);
679 }
680
681 pub(crate) fn queue_reparse(&self, scope: crate::config::ReparseScope) -> u64 {
693 {
694 let mut pending = self
695 .pending_reparse
696 .lock()
697 .unwrap_or_else(std::sync::PoisonError::into_inner);
698 *pending = Some(match pending.take() {
699 Some(existing) => PendingReparse {
700 scope: existing.scope.union(scope),
701 first_queued_at: existing.first_queued_at,
702 },
703 None => PendingReparse {
704 scope,
705 first_queued_at: Instant::now(),
706 },
707 });
708 }
709 self.config_generation.fetch_add(1, Ordering::SeqCst) + 1
710 }
711
712 pub(crate) fn config_generation(&self) -> u64 {
716 self.config_generation.load(Ordering::SeqCst)
717 }
718
719 pub(crate) fn pending_reparse_overdue(&self, max_wait: Duration) -> bool {
728 self.pending_reparse
729 .lock()
730 .unwrap_or_else(std::sync::PoisonError::into_inner)
731 .as_ref()
732 .is_some_and(|pending| pending.first_queued_at.elapsed() >= max_wait)
733 }
734
735 pub(crate) fn take_pending_reparse(&self) -> Option<crate::config::ReparseScope> {
743 self.pending_reparse
744 .lock()
745 .unwrap_or_else(std::sync::PoisonError::into_inner)
746 .take()
747 .map(|pending| pending.scope)
748 }
749
750 pub fn spawn_refresh_requests(&self, client: &Client) {
761 if self.inlay_hint_refresh_supported() {
762 let client = client.clone();
763 tokio::spawn(async move {
764 match tokio::time::timeout(CLIENT_REFRESH_TIMEOUT, client.inlay_hint_refresh())
765 .await
766 {
767 Ok(Ok(())) => {}
768 Ok(Err(e)) => tracing::debug!("inlay_hint_refresh failed: {:?}", e),
769 Err(_) => tracing::debug!(
770 "inlay_hint_refresh timed out after {CLIENT_REFRESH_TIMEOUT:?}"
771 ),
772 }
773 });
774 }
775 if self.code_lens_refresh_supported() {
776 let client = client.clone();
777 tokio::spawn(async move {
778 match tokio::time::timeout(CLIENT_REFRESH_TIMEOUT, client.code_lens_refresh()).await
779 {
780 Ok(Ok(())) => {}
781 Ok(Err(e)) => tracing::debug!("code_lens_refresh failed: {:?}", e),
782 Err(_) => tracing::debug!(
783 "code_lens_refresh timed out after {CLIENT_REFRESH_TIMEOUT:?}"
784 ),
785 }
786 });
787 }
788 }
789
790 pub fn get_document(
798 &self,
799 uri: &Uri,
800 ) -> Option<dashmap::mapref::one::Ref<'_, Uri, DocumentState>> {
801 self.documents.get(uri)
802 }
803
804 pub fn with_document<T>(
828 &self,
829 uri: &Uri,
830 extract: impl FnOnce(&DocumentState) -> T,
831 ) -> Option<T> {
832 self.documents.get(uri).map(|doc| extract(&doc))
833 }
834
835 pub fn get_document_clone(&self, uri: &Uri) -> Option<DocumentState> {
865 self.documents.get(uri).map(|doc| doc.clone())
866 }
867
868 pub fn update_document(&self, uri: Uri, state: DocumentState) {
873 self.documents.insert(uri, state);
874 }
875
876 pub fn remove_document(&self, uri: &Uri) -> Option<(Uri, DocumentState)> {
880 self.documents.remove(uri)
881 }
882
883 pub async fn spawn_background_task(&self, uri: Uri, task: JoinHandle<()>) {
892 let mut tasks = self.tasks.write().await;
893
894 if let Some(old_task) = tasks.remove(&uri) {
896 old_task.abort();
897 }
898
899 tasks.insert(uri, task);
900 }
901
902 pub async fn cancel_background_task(&self, uri: &Uri) {
906 let mut tasks = self.tasks.write().await;
907 if let Some(task) = tasks.remove(uri) {
908 task.abort();
909 }
910 }
911
912 pub fn document_count(&self) -> usize {
914 self.documents.len()
915 }
916}
917
918impl Default for ServerState {
919 fn default() -> Self {
920 Self::new()
921 }
922}
923
924#[cfg(test)]
925mod tests {
926 use super::*;
927
928 use std::assert_matches;
929
930 mod loading_state_tests {
939 use super::*;
940
941 #[test]
942 fn test_loading_state_default() {
943 let state = LoadingState::default();
944 assert_eq!(state, LoadingState::Idle);
945 }
946
947 #[test]
948 fn test_loading_state_transitions() {
949 use std::time::Duration;
950
951 let content = "[dependencies]\nserde = \"1.0\"".to_string();
952 let mut doc = DocumentState::new_without_parse_result(EcosystemId::Cargo, content);
953
954 assert_eq!(doc.loading_state, LoadingState::Idle);
956 assert!(doc.loading_started_at.is_none());
957
958 doc.set_loading();
960 assert_eq!(doc.loading_state, LoadingState::Loading);
961 assert!(doc.loading_started_at.is_some());
962
963 std::thread::sleep(Duration::from_millis(10));
965
966 let duration = doc.loading_duration();
968 assert!(duration.is_some());
969 assert!(duration.unwrap() >= Duration::from_millis(10));
970
971 doc.set_loaded();
973 assert_eq!(doc.loading_state, LoadingState::Loaded);
974 assert!(doc.loading_started_at.is_none());
975 assert!(doc.loading_duration().is_none());
976 }
977
978 #[test]
979 fn test_loading_state_failed_transition() {
980 let content = "[dependencies]\nserde = \"1.0\"".to_string();
981 let mut doc = DocumentState::new_without_parse_result(EcosystemId::Cargo, content);
982
983 doc.set_loading();
984 assert_eq!(doc.loading_state, LoadingState::Loading);
985
986 doc.set_failed();
987 assert_eq!(doc.loading_state, LoadingState::Failed);
988 assert!(doc.loading_started_at.is_none());
989 }
990
991 #[test]
992 fn test_loading_state_clone() {
993 let content = "[dependencies]\nserde = \"1.0\"".to_string();
994 let mut doc = DocumentState::new_without_parse_result(EcosystemId::Cargo, content);
995
996 doc.set_loading();
997 let cloned = doc.clone();
998
999 assert_eq!(cloned.loading_state, LoadingState::Loading);
1000 assert!(cloned.loading_started_at.is_some());
1001 }
1002
1003 #[test]
1004 fn test_loading_state_debug() {
1005 let content = "[dependencies]\nserde = \"1.0\"".to_string();
1006 let mut doc = DocumentState::new_without_parse_result(EcosystemId::Cargo, content);
1007 doc.set_loading();
1008
1009 let debug_str = format!("{:?}", doc);
1010 assert!(debug_str.contains("loading_state"));
1011 assert!(debug_str.contains("Loading"));
1012 }
1013
1014 #[test]
1015 fn test_loading_duration_none_when_idle() {
1016 let content = "[dependencies]\nserde = \"1.0\"".to_string();
1017 let doc = DocumentState::new_without_parse_result(EcosystemId::Cargo, content);
1018
1019 assert_eq!(doc.loading_state, LoadingState::Idle);
1020 assert!(doc.loading_duration().is_none());
1021 }
1022
1023 #[test]
1024 fn test_loading_state_equality() {
1025 assert_eq!(LoadingState::Idle, LoadingState::Idle);
1026 assert_eq!(LoadingState::Loading, LoadingState::Loading);
1027 assert_eq!(LoadingState::Loaded, LoadingState::Loaded);
1028 assert_eq!(LoadingState::Failed, LoadingState::Failed);
1029
1030 assert_ne!(LoadingState::Idle, LoadingState::Loading);
1031 assert_ne!(LoadingState::Loading, LoadingState::Loaded);
1032 }
1033
1034 #[test]
1035 fn test_loading_duration_tracks_time_correctly() {
1036 use std::time::Duration;
1037
1038 let content = "[dependencies]\nserde = \"1.0\"".to_string();
1039 let mut doc = DocumentState::new_without_parse_result(EcosystemId::Cargo, content);
1040
1041 doc.set_loading();
1042
1043 let duration1 = doc.loading_duration().unwrap();
1045 std::thread::sleep(Duration::from_millis(20));
1046 let duration2 = doc.loading_duration().unwrap();
1047
1048 assert!(duration2 > duration1, "Duration should increase over time");
1049 }
1050
1051 #[tokio::test]
1052 async fn test_concurrent_loading_state_mutations() {
1053 use std::sync::Arc;
1054 use tokio::sync::Barrier;
1055
1056 let state = Arc::new(ServerState::new());
1057 let uri = deps_core::test_util::test_uri("/concurrent-loading-test.toml");
1058
1059 let doc = DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1060 state.update_document(uri.clone(), doc);
1061
1062 let barrier = Arc::new(Barrier::new(10));
1063 let mut handles = vec![];
1064
1065 for i in 0..10 {
1066 let state_clone = Arc::clone(&state);
1067 let uri_clone = uri.clone();
1068 let barrier_clone = Arc::clone(&barrier);
1069
1070 handles.push(tokio::spawn(async move {
1071 barrier_clone.wait().await;
1072 if let Some(mut doc) = state_clone.documents.get_mut(&uri_clone) {
1073 if i % 3 == 0 {
1074 doc.set_loading();
1075 } else if i % 3 == 1 {
1076 doc.set_loaded();
1077 } else {
1078 doc.set_failed();
1079 }
1080 }
1081 }));
1082 }
1083
1084 for handle in handles {
1085 handle.await.unwrap();
1086 }
1087
1088 let doc = state.get_document(&uri).unwrap();
1089 assert_matches!(
1090 doc.loading_state,
1091 LoadingState::Idle
1092 | LoadingState::Loading
1093 | LoadingState::Loaded
1094 | LoadingState::Failed
1095 );
1096 }
1097
1098 #[test]
1099 fn test_set_loaded_idempotent() {
1100 let mut doc =
1101 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1102
1103 doc.set_loading();
1104 doc.set_loaded();
1105
1106 doc.set_loaded();
1108
1109 assert_eq!(doc.loading_state, LoadingState::Loaded);
1110 assert!(doc.loading_started_at.is_none());
1111 }
1112
1113 #[test]
1114 fn test_set_loading_resets_timer() {
1115 let mut doc =
1116 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1117
1118 doc.set_loading();
1119 let first_start = doc.loading_started_at.unwrap();
1120
1121 std::thread::sleep(std::time::Duration::from_millis(10));
1122
1123 doc.set_loading();
1125 let second_start = doc.loading_started_at.unwrap();
1126
1127 assert!(second_start > first_start, "Timer should be reset");
1128 assert_eq!(doc.loading_state, LoadingState::Loading);
1129 }
1130
1131 #[test]
1132 fn test_retry_after_failure() {
1133 let mut doc =
1134 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1135
1136 doc.set_loading();
1137 doc.set_failed();
1138 assert_eq!(doc.loading_state, LoadingState::Failed);
1139 assert!(doc.loading_started_at.is_none());
1140
1141 doc.set_loading();
1143 assert_eq!(doc.loading_state, LoadingState::Loading);
1144 assert!(doc.loading_started_at.is_some());
1145
1146 doc.set_loaded();
1147 assert_eq!(doc.loading_state, LoadingState::Loaded);
1148 }
1149
1150 #[test]
1151 fn test_refresh_after_loaded() {
1152 let mut doc =
1153 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1154
1155 doc.set_loading();
1156 doc.set_loaded();
1157 assert_eq!(doc.loading_state, LoadingState::Loaded);
1158
1159 doc.set_loading();
1161 assert_eq!(doc.loading_state, LoadingState::Loading);
1162 assert!(doc.loading_started_at.is_some());
1163
1164 doc.set_loaded();
1165 assert_eq!(doc.loading_state, LoadingState::Loaded);
1166 }
1167 }
1168
1169 mod is_ready_for_batch_update_tests {
1175 use super::*;
1176
1177 #[test]
1178 fn test_not_ready_without_a_version() {
1179 let mut doc =
1180 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1181 doc.set_loaded();
1182 assert!(!doc.is_ready_for_batch_update());
1183 }
1184
1185 #[test]
1186 fn test_not_ready_while_loading_even_with_a_version() {
1187 let mut doc =
1188 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1189 doc.set_version(Some(1));
1190 doc.set_loading();
1191 assert!(!doc.is_ready_for_batch_update());
1192 }
1193
1194 #[test]
1195 fn test_ready_when_loaded_with_a_version() {
1196 let mut doc =
1197 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1198 doc.set_version(Some(1));
1199 doc.set_loaded();
1200 assert!(doc.is_ready_for_batch_update());
1201 }
1202
1203 #[test]
1204 fn test_ready_when_failed_with_a_version() {
1205 let mut doc =
1210 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1211 doc.set_version(Some(1));
1212 doc.set_failed();
1213 assert!(doc.is_ready_for_batch_update());
1214 }
1215
1216 #[test]
1217 fn test_not_ready_without_version_or_loaded_state() {
1218 let doc = DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
1219 assert!(!doc.is_ready_for_batch_update());
1220 }
1221 }
1222
1223 #[test]
1224 fn test_server_state_creation() {
1225 let state = ServerState::new();
1226 assert_eq!(state.document_count(), 0);
1227 assert!(state.cache.is_empty(), "Cache should start empty");
1228 }
1229
1230 #[test]
1231 fn test_server_state_default() {
1232 let state = ServerState::default();
1233 assert_eq!(state.document_count(), 0);
1234 }
1235
1236 #[test]
1242 fn test_inlay_hint_refresh_supported_defaults_false_and_round_trips() {
1243 let state = ServerState::new();
1244 assert!(!state.inlay_hint_refresh_supported());
1245
1246 state.set_inlay_hint_refresh_supported(true);
1247 assert!(state.inlay_hint_refresh_supported());
1248
1249 state.set_inlay_hint_refresh_supported(false);
1250 assert!(!state.inlay_hint_refresh_supported());
1251 }
1252
1253 #[test]
1254 fn test_code_lens_refresh_supported_defaults_false_and_round_trips() {
1255 let state = ServerState::new();
1256 assert!(!state.code_lens_refresh_supported());
1257
1258 state.set_code_lens_refresh_supported(true);
1259 assert!(state.code_lens_refresh_supported());
1260
1261 state.set_code_lens_refresh_supported(false);
1262 assert!(!state.code_lens_refresh_supported());
1263 }
1264
1265 #[test]
1266 fn test_diagnostic_refresh_supported_defaults_false_and_round_trips() {
1267 let state = ServerState::new();
1268 assert!(!state.diagnostic_refresh_supported());
1269
1270 state.set_diagnostic_refresh_supported(true);
1271 assert!(state.diagnostic_refresh_supported());
1272
1273 state.set_diagnostic_refresh_supported(false);
1274 assert!(!state.diagnostic_refresh_supported());
1275 }
1276
1277 mod reparse_coalescing_tests {
1282 use super::*;
1283 use crate::config::ReparseScope;
1284
1285 #[test]
1286 fn test_queue_reparse_bumps_generation() {
1287 let state = ServerState::new();
1288 assert_eq!(state.config_generation(), 0);
1289
1290 let gen1 = state.queue_reparse(ReparseScope::Ecosystems(vec!["cargo"]));
1291 assert_eq!(gen1, 1);
1292 assert_eq!(state.config_generation(), 1);
1293
1294 let gen2 = state.queue_reparse(ReparseScope::Ecosystems(vec!["npm"]));
1295 assert_eq!(gen2, 2);
1296 }
1297
1298 #[test]
1299 fn test_queue_reparse_unions_pending_scope_across_calls() {
1300 let state = ServerState::new();
1301 state.queue_reparse(ReparseScope::Ecosystems(vec!["cargo"]));
1302 state.queue_reparse(ReparseScope::Ecosystems(vec!["npm"]));
1303
1304 let scope = state
1305 .take_pending_reparse()
1306 .expect("a pending scope must exist after two queue_reparse calls");
1307 assert!(
1308 scope.matches("cargo"),
1309 "earlier change's scope must survive"
1310 );
1311 assert!(scope.matches("npm"));
1312 }
1313
1314 #[test]
1315 fn test_take_pending_reparse_drains_and_returns_none_on_empty() {
1316 let state = ServerState::new();
1317 assert!(state.take_pending_reparse().is_none(), "nothing queued yet");
1318
1319 state.queue_reparse(ReparseScope::All);
1320 assert!(state.take_pending_reparse().is_some());
1321 assert!(
1322 state.take_pending_reparse().is_none(),
1323 "a second drain must see nothing left (M1: never unwrap this)"
1324 );
1325 }
1326
1327 #[test]
1330 fn test_pending_reparse_overdue_false_when_nothing_queued() {
1331 let state = ServerState::new();
1332 assert!(!state.pending_reparse_overdue(Duration::ZERO));
1333 }
1334
1335 #[tokio::test]
1339 async fn test_pending_reparse_overdue_becomes_true_after_max_wait_elapses() {
1340 let state = ServerState::new();
1341 state.queue_reparse(ReparseScope::Ecosystems(vec!["cargo"]));
1342
1343 assert!(
1344 !state.pending_reparse_overdue(Duration::from_secs(10)),
1345 "must not be overdue against a generous max_wait"
1346 );
1347
1348 tokio::time::sleep(Duration::from_millis(5)).await;
1349 assert!(
1350 state.pending_reparse_overdue(Duration::from_millis(1)),
1351 "must be overdue once real elapsed time exceeds max_wait"
1352 );
1353 }
1354
1355 #[tokio::test]
1360 async fn test_queue_reparse_union_preserves_first_queued_at() {
1361 let state = ServerState::new();
1362 state.queue_reparse(ReparseScope::Ecosystems(vec!["cargo"]));
1363
1364 tokio::time::sleep(Duration::from_millis(20)).await;
1365 state.queue_reparse(ReparseScope::Ecosystems(vec!["npm"]));
1369
1370 assert!(
1371 state.pending_reparse_overdue(Duration::from_millis(15)),
1372 "overdue must be measured from the first queued change, not the latest union"
1373 );
1374 }
1375
1376 #[tokio::test]
1377 async fn test_fetch_permits_bounds_concurrency() {
1378 let state = Arc::new(ServerState::new());
1379 assert_eq!(state.fetch_permits.available_permits(), 4);
1380
1381 let permit = state.fetch_permits.acquire().await.unwrap();
1382 assert_eq!(state.fetch_permits.available_permits(), 3);
1383 drop(permit);
1384 assert_eq!(state.fetch_permits.available_permits(), 4);
1385 }
1386
1387 #[tokio::test]
1394 async fn test_fetch_permits_bounds_real_concurrent_contention() {
1395 use std::sync::atomic::AtomicUsize;
1396
1397 let state = Arc::new(ServerState::new());
1398 let current = Arc::new(AtomicUsize::new(0));
1399 let max_seen = Arc::new(AtomicUsize::new(0));
1400
1401 let mut handles = Vec::new();
1402 for _ in 0..10 {
1403 let state = Arc::clone(&state);
1404 let current = Arc::clone(¤t);
1405 let max_seen = Arc::clone(&max_seen);
1406 handles.push(tokio::spawn(async move {
1407 let _permit = state.fetch_permits.acquire().await.unwrap();
1408 let now = current.fetch_add(1, Ordering::SeqCst) + 1;
1409 max_seen.fetch_max(now, Ordering::SeqCst);
1410 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
1411 current.fetch_sub(1, Ordering::SeqCst);
1412 }));
1413 }
1414 for handle in handles {
1415 handle.await.unwrap();
1416 }
1417
1418 let peak = max_seen.load(Ordering::SeqCst);
1419 assert!(
1420 peak <= 4,
1421 "peak concurrent permit-holders ({peak}) must never exceed FETCH_PERMITS \
1422 (4) — the semaphore failed to bound concurrency"
1423 );
1424 assert!(
1425 peak >= 2,
1426 "peak concurrent permit-holders ({peak}) is implausibly low for 10 tasks \
1427 racing for 4 permits — this test isn't exercising real contention (a \
1428 liveness check, not exact equality, since scheduler timing shouldn't pin \
1429 the assertion to exactly 4)"
1430 );
1431 }
1432 }
1433
1434 #[tokio::test]
1435 async fn test_server_state_background_tasks() {
1436 let state = ServerState::new();
1437 let uri = deps_core::test_util::test_uri("/test.toml");
1438
1439 let task = tokio::spawn(async {
1440 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1441 });
1442
1443 state.spawn_background_task(uri.clone(), task).await;
1444 state.cancel_background_task(&uri).await;
1445 }
1446
1447 #[tokio::test]
1448 async fn test_spawn_background_task_cancels_previous() {
1449 let state = ServerState::new();
1450 let uri = deps_core::test_util::test_uri("/test.toml");
1451
1452 let task1 = tokio::spawn(async {
1453 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1454 });
1455 state.spawn_background_task(uri.clone(), task1).await;
1456
1457 let task2 = tokio::spawn(async {
1458 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1459 });
1460 state.spawn_background_task(uri.clone(), task2).await;
1461 state.cancel_background_task(&uri).await;
1462 }
1463
1464 #[tokio::test]
1465 async fn test_cancel_background_task_nonexistent() {
1466 let state = ServerState::new();
1467 let uri = deps_core::test_util::test_uri("/test.toml");
1468 state.cancel_background_task(&uri).await;
1469 }
1470
1471 mod cold_start_limiter {
1476 use super::*;
1477 use std::time::Duration;
1478
1479 #[test]
1480 fn test_allows_first_request() {
1481 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1482 let uri = deps_core::test_util::test_uri("/test.toml");
1483 assert!(
1484 limiter.allow_cold_start(&uri),
1485 "First request should be allowed"
1486 );
1487 }
1488
1489 #[test]
1490 fn test_blocks_rapid_requests() {
1491 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1492 let uri = deps_core::test_util::test_uri("/test.toml");
1493
1494 assert!(limiter.allow_cold_start(&uri), "First request allowed");
1495 assert!(
1496 !limiter.allow_cold_start(&uri),
1497 "Second immediate request should be blocked"
1498 );
1499 }
1500
1501 #[tokio::test]
1502 async fn test_allows_after_interval() {
1503 let limiter = ColdStartLimiter::new(Duration::from_millis(50));
1504 let uri = deps_core::test_util::test_uri("/test.toml");
1505
1506 assert!(limiter.allow_cold_start(&uri), "First request allowed");
1507 tokio::time::sleep(Duration::from_millis(60)).await;
1508 assert!(
1509 limiter.allow_cold_start(&uri),
1510 "Request after interval should be allowed"
1511 );
1512 }
1513
1514 #[tokio::test]
1517 async fn test_set_min_interval_changes_behavior() {
1518 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1519 let uri = deps_core::test_util::test_uri("/test.toml");
1520
1521 assert!(limiter.allow_cold_start(&uri), "First request allowed");
1522 assert!(
1523 !limiter.allow_cold_start(&uri),
1524 "Second immediate request blocked under the original 100ms interval"
1525 );
1526
1527 limiter.set_min_interval(Duration::ZERO);
1531
1532 assert!(
1533 limiter.allow_cold_start(&uri),
1534 "Lowering the interval to 0 should allow every request immediately"
1535 );
1536 assert!(
1537 limiter.allow_cold_start(&uri),
1538 "A zero interval keeps allowing consecutive requests"
1539 );
1540 }
1541
1542 #[test]
1543 fn test_different_uris_independent() {
1544 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1545 let uri1 = deps_core::test_util::test_uri("/test1.toml");
1546 let uri2 = deps_core::test_util::test_uri("/test2.toml");
1547
1548 assert!(limiter.allow_cold_start(&uri1), "URI 1 first request");
1549 assert!(limiter.allow_cold_start(&uri2), "URI 2 first request");
1550 assert!(
1551 !limiter.allow_cold_start(&uri1),
1552 "URI 1 second request blocked"
1553 );
1554 assert!(
1555 !limiter.allow_cold_start(&uri2),
1556 "URI 2 second request blocked"
1557 );
1558 }
1559
1560 #[test]
1561 fn test_cleanup() {
1562 let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1563 let uri1 = deps_core::test_util::test_uri("/test1.toml");
1564 let uri2 = deps_core::test_util::test_uri("/test2.toml");
1565
1566 limiter.allow_cold_start(&uri1);
1567 limiter.allow_cold_start(&uri2);
1568 assert_eq!(limiter.tracked_count(), 2, "Should track 2 URIs");
1569
1570 limiter.cleanup_old_entries(Duration::from_millis(0));
1571 assert_eq!(
1572 limiter.tracked_count(),
1573 0,
1574 "All entries should be cleaned up"
1575 );
1576 }
1577
1578 #[tokio::test]
1579 async fn test_concurrent_access() {
1580 use std::sync::Arc;
1581
1582 let limiter = Arc::new(ColdStartLimiter::new(Duration::from_millis(100)));
1583 let uri = deps_core::test_util::test_uri("/concurrent-test.toml");
1584
1585 let mut handles = vec![];
1586 const CONCURRENT_TASKS: usize = 10;
1587
1588 for _ in 0..CONCURRENT_TASKS {
1589 let limiter_clone = Arc::clone(&limiter);
1590 let uri_clone = uri.clone();
1591 let handle =
1592 tokio::spawn(async move { limiter_clone.allow_cold_start(&uri_clone) });
1593 handles.push(handle);
1594 }
1595
1596 let mut results = vec![];
1597 for handle in handles {
1598 results.push(handle.await.unwrap());
1599 }
1600
1601 let allowed_count = results.iter().filter(|&&allowed| allowed).count();
1602 assert_eq!(allowed_count, 1, "Exactly one concurrent request allowed");
1603
1604 let blocked_count = results.iter().filter(|&&allowed| !allowed).count();
1605 assert_eq!(
1606 blocked_count,
1607 CONCURRENT_TASKS - 1,
1608 "Rest should be blocked"
1609 );
1610 }
1611 }
1612
1613 #[test]
1625 fn test_document_state_new_without_parse_result_resolves_all_ecosystems() {
1626 for (id, expected) in [
1627 ("cargo", EcosystemId::Cargo),
1628 ("npm", EcosystemId::Npm),
1629 ("pypi", EcosystemId::Pypi),
1630 ("go", EcosystemId::Go),
1631 ("bundler", EcosystemId::Bundler),
1632 ("dart", EcosystemId::Dart),
1633 ("maven", EcosystemId::Maven),
1634 ("composer", EcosystemId::Composer),
1635 ("gradle", EcosystemId::Gradle),
1636 ("swift", EcosystemId::Swift),
1637 ("nuget", EcosystemId::NuGet),
1638 ("deno", EcosystemId::Deno),
1639 ("github-actions", EcosystemId::GithubActions),
1640 ] {
1641 let parsed: EcosystemId = id
1642 .parse()
1643 .unwrap_or_else(|_| panic!("ecosystem_id {id:?} failed to parse"));
1644 assert_eq!(parsed, expected, "ecosystem_id {id:?} misresolved");
1645
1646 let doc = DocumentState::new_without_parse_result(expected, String::new());
1647 assert_eq!(doc.ecosystem, expected);
1648 assert_eq!(doc.ecosystem_id(), id);
1649 }
1650 }
1651
1652 #[cfg(feature = "maven")]
1657 #[test]
1658 fn test_document_state_new_from_parse_result_maven_not_misclassified_as_cargo() {
1659 let state = ServerState::new();
1660 let uri = deps_core::test_util::test_uri("/test/pom.xml");
1661 let ecosystem = state.ecosystem_registry.get("maven").unwrap();
1662 let content = r"<project>
1663 <dependencies>
1664 <dependency>
1665 <groupId>org.apache.commons</groupId>
1666 <artifactId>commons-lang3</artifactId>
1667 <version>3.12.0</version>
1668 </dependency>
1669 </dependencies>
1670</project>
1671"
1672 .to_string();
1673
1674 let parse_result = tokio::runtime::Runtime::new()
1675 .unwrap()
1676 .block_on(ecosystem.parse_manifest(&content, &uri))
1677 .unwrap();
1678
1679 let ecosystem_id: EcosystemId = "maven"
1680 .parse()
1681 .expect("maven must resolve to an EcosystemId");
1682 let doc_state = DocumentState::new_from_parse_result(ecosystem_id, content, parse_result);
1683
1684 assert_eq!(doc_state.ecosystem_id(), "maven");
1685 assert_eq!(doc_state.ecosystem, EcosystemId::Maven);
1686 }
1687
1688 #[cfg(feature = "cargo")]
1693 mod cargo_tests {
1694 use super::*;
1695
1696 #[test]
1697 fn test_document_state_creation() {
1698 let state =
1699 DocumentState::new_without_parse_result(EcosystemId::Cargo, "test content".into());
1700
1701 assert_eq!(state.ecosystem, EcosystemId::Cargo);
1702 assert_eq!(state.content, "test content");
1703 assert!(state.cached_versions.is_empty());
1704 }
1705
1706 #[test]
1707 fn test_server_state_document_operations() {
1708 let state = ServerState::new();
1709 let uri = deps_core::test_util::test_uri("/test.toml");
1710 let doc_state =
1711 DocumentState::new_without_parse_result(EcosystemId::Cargo, "test".into());
1712
1713 state.update_document(uri.clone(), doc_state);
1714 assert_eq!(state.document_count(), 1);
1715
1716 let retrieved = state.get_document(&uri);
1717 assert!(retrieved.is_some());
1718 assert_eq!(retrieved.unwrap().content, "test");
1719
1720 let removed = state.remove_document(&uri);
1721 assert!(removed.is_some());
1722 assert_eq!(state.document_count(), 0);
1723 }
1724
1725 #[test]
1726 fn test_document_state_new_from_parse_result() {
1727 let state = ServerState::new();
1728 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
1729 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
1730 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1731
1732 let parse_result = tokio::runtime::Runtime::new()
1733 .unwrap()
1734 .block_on(ecosystem.parse_manifest(&content, &uri))
1735 .unwrap();
1736
1737 let doc_state = DocumentState::new_from_parse_result(
1738 EcosystemId::Cargo,
1739 content.clone(),
1740 parse_result,
1741 );
1742
1743 assert_eq!(doc_state.ecosystem_id(), "cargo");
1744 assert_eq!(doc_state.content, content);
1745 assert!(doc_state.parse_result.is_some());
1746 }
1747
1748 #[test]
1749 fn test_document_state_new_without_parse_result() {
1750 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1751 let doc_state = DocumentState::new_without_parse_result(EcosystemId::Cargo, content);
1752
1753 assert_eq!(doc_state.ecosystem_id(), "cargo");
1754 assert_eq!(doc_state.ecosystem, EcosystemId::Cargo);
1755 assert!(doc_state.parse_result.is_none());
1756 }
1757
1758 #[test]
1759 fn test_document_state_update_resolved_versions() {
1760 let mut state =
1761 DocumentState::new_without_parse_result(EcosystemId::Cargo, "test".into());
1762
1763 let mut resolved = HashMap::new();
1764 resolved.insert("serde".into(), "1.0.195".into());
1765
1766 state.update_resolved_versions(resolved);
1767 assert_eq!(state.resolved_versions.len(), 1);
1768 assert_eq!(
1769 state.resolved_versions.get("serde"),
1770 Some(&"1.0.195".into())
1771 );
1772 }
1773
1774 #[test]
1775 fn test_document_state_update_cached_versions() {
1776 let mut state =
1777 DocumentState::new_without_parse_result(EcosystemId::Cargo, "test".into());
1778
1779 let mut cached = HashMap::new();
1780 cached.insert("serde".into(), PackageVersions::latest_only("1.0.210"));
1781
1782 state.update_cached_versions(cached);
1783 assert_eq!(state.cached_versions.len(), 1);
1784 }
1785
1786 #[test]
1787 fn test_document_state_parse_result_accessor() {
1788 let state = DocumentState::new_without_parse_result(EcosystemId::Cargo, "test".into());
1789 assert!(state.parse_result().is_none());
1790 }
1791
1792 #[test]
1793 fn test_document_state_clone() {
1794 let state =
1795 DocumentState::new_without_parse_result(EcosystemId::Cargo, "test content".into());
1796 let cloned = state.clone();
1797
1798 assert_eq!(cloned.ecosystem, state.ecosystem);
1799 assert_eq!(cloned.content, state.content);
1800 assert!(cloned.parse_result.is_none());
1801 }
1802
1803 #[test]
1807 fn test_document_state_clone_preserves_parse_result() {
1808 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
1809 let ecosystem = ServerState::new().ecosystem_registry.get("cargo").unwrap();
1810 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1811 let parse_result = tokio::runtime::Runtime::new()
1812 .unwrap()
1813 .block_on(ecosystem.parse_manifest(&content, &uri))
1814 .unwrap();
1815
1816 let state =
1817 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
1818 let cloned = state.clone();
1819
1820 assert!(
1821 cloned.parse_result_arc().is_some(),
1822 "clone must still carry the parse result"
1823 );
1824 assert!(std::sync::Arc::ptr_eq(
1825 &state.parse_result_arc().unwrap(),
1826 &cloned.parse_result_arc().unwrap()
1827 ));
1828 }
1829
1830 #[test]
1831 fn test_document_state_debug() {
1832 let state = DocumentState::new_without_parse_result(EcosystemId::Cargo, "test".into());
1833 let debug_str = format!("{state:?}");
1834 assert!(debug_str.contains("DocumentState"));
1835 }
1836 }
1837
1838 #[cfg(feature = "npm")]
1843 mod npm_tests {
1844 use super::*;
1845
1846 #[test]
1847 fn test_document_state_new_without_parse_result() {
1848 let content = r#"{"dependencies": {"express": "^4.18.0"}}"#.to_string();
1849 let doc_state = DocumentState::new_without_parse_result(EcosystemId::Npm, content);
1850
1851 assert_eq!(doc_state.ecosystem_id(), "npm");
1852 assert_eq!(doc_state.ecosystem, EcosystemId::Npm);
1853 assert!(doc_state.parse_result.is_none());
1854 }
1855 }
1856
1857 #[cfg(feature = "deno")]
1862 mod deno_tests {
1863 use super::*;
1864
1865 #[test]
1866 fn test_document_state_new_without_parse_result() {
1867 let content = r#"{"imports": {"@std/fs": "jsr:@std/fs@^1.0"}}"#.to_string();
1868 let doc_state = DocumentState::new_without_parse_result(EcosystemId::Deno, content);
1869
1870 assert_eq!(doc_state.ecosystem_id(), "deno");
1871 assert_eq!(doc_state.ecosystem, EcosystemId::Deno);
1872 assert!(doc_state.parse_result.is_none());
1873 }
1874 }
1875
1876 #[cfg(feature = "pypi")]
1881 mod pypi_tests {
1882 use super::*;
1883
1884 #[test]
1885 fn test_document_state_new_without_parse_result() {
1886 let content = "[project]\ndependencies = [\"requests>=2.0.0\"]\n".to_string();
1887 let doc_state = DocumentState::new_without_parse_result(EcosystemId::Pypi, content);
1888
1889 assert_eq!(doc_state.ecosystem_id(), "pypi");
1890 assert_eq!(doc_state.ecosystem, EcosystemId::Pypi);
1891 assert!(doc_state.parse_result.is_none());
1892 }
1893 }
1894
1895 #[cfg(feature = "go")]
1900 mod go_tests {
1901 use super::*;
1902
1903 #[test]
1904 fn test_document_state_new_without_parse_result() {
1905 let content =
1906 "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1907 .to_string();
1908 let doc_state = DocumentState::new_without_parse_result(EcosystemId::Go, content);
1909
1910 assert_eq!(doc_state.ecosystem_id(), "go");
1911 assert_eq!(doc_state.ecosystem, EcosystemId::Go);
1912 assert!(doc_state.parse_result.is_none());
1913 }
1914
1915 #[test]
1916 fn test_document_state_new_from_parse_result() {
1917 let state = ServerState::new();
1918 let uri = deps_core::test_util::test_uri("/test/go.mod");
1919 let ecosystem = state.ecosystem_registry.get("go").unwrap();
1920 let content =
1921 "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1922 .to_string();
1923
1924 let parse_result = tokio::runtime::Runtime::new()
1925 .unwrap()
1926 .block_on(ecosystem.parse_manifest(&content, &uri))
1927 .unwrap();
1928
1929 let doc_state = DocumentState::new_from_parse_result(
1930 EcosystemId::Go,
1931 content.clone(),
1932 parse_result,
1933 );
1934
1935 assert_eq!(doc_state.ecosystem_id(), "go");
1936 assert!(doc_state.parse_result.is_some());
1937 }
1938 }
1939}