1use std::collections::{BTreeMap, HashMap, VecDeque};
6
7use chrono::{DateTime, Utc};
8use lsp_types::{Diagnostic as LspDiagnostic, Uri};
9use serde::{Deserialize, Serialize};
10use tracing::warn;
11
12use crate::config::ServerId;
13use crate::util::{truncate_str, truncate_string};
14
15const MAX_LOG_ENTRIES: usize = 100;
17
18const MAX_ENTRY_TEXT_BYTES: usize = 256 * 1024;
34
35const MAX_DIAGNOSTICS_ENTRY_BYTES: usize = 1024 * 1024;
55
56const MAX_DIAGNOSTIC_ENTRIES: usize = 1000;
80
81fn uri_cache_key(uri: &str) -> std::borrow::Cow<'_, str> {
89 if cfg!(windows) {
90 std::borrow::Cow::Owned(uri.to_ascii_lowercase())
91 } else {
92 std::borrow::Cow::Borrowed(uri)
93 }
94}
95
96const MAX_SERVER_MESSAGES: usize = 50;
98
99const DIAGNOSTIC_ESTIMATE_OVERHEAD_BYTES: usize = 256;
106
107const JSON_ESCAPE_WORST_CASE_FACTOR: usize = 6;
121
122const DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES: usize = 1024;
128
129const fn diagnostic_severity_rank(diagnostic: &LspDiagnostic) -> u8 {
140 match diagnostic.severity {
141 Some(lsp_types::DiagnosticSeverity::ERROR) => 0,
142 Some(lsp_types::DiagnosticSeverity::WARNING) => 1,
143 Some(lsp_types::DiagnosticSeverity::INFORMATION) => 2,
144 Some(_) | None => 3,
147 }
148}
149
150fn largest_fitting_prefix(
161 diagnostics: &[LspDiagnostic],
162 fits: impl Fn(&[LspDiagnostic]) -> bool,
163) -> usize {
164 let (mut lo, mut hi) = (0usize, diagnostics.len());
165 while lo < hi {
166 let mid = lo + (hi - lo).div_ceil(2);
167 if fits(&diagnostics[..mid]) {
168 lo = mid;
169 } else {
170 hi = mid - 1;
171 }
172 }
173 lo
174}
175
176fn cap_diagnostics_entry_size(uri: &Uri, diagnostics: &mut Vec<LspDiagnostic>) {
219 let fits = |ds: &[LspDiagnostic]| {
220 serde_json::to_vec(ds).is_ok_and(|bytes| bytes.len() <= MAX_DIAGNOSTICS_ENTRY_BYTES)
227 };
228
229 let cheaply_estimable = diagnostics.iter().all(|d| {
230 d.data.is_none()
231 && d.code_description.is_none()
232 && d.related_information.is_none()
233 && d.tags.is_none()
234 });
235 if cheaply_estimable {
236 let estimated: usize = diagnostics
237 .iter()
238 .map(|d| {
239 let raw_string_bytes = d.message.len()
240 + d.source.as_deref().map_or(0, str::len)
241 + match &d.code {
242 Some(lsp_types::NumberOrString::String(s)) => s.len(),
243 _ => 0,
244 };
245 raw_string_bytes * JSON_ESCAPE_WORST_CASE_FACTOR
246 + DIAGNOSTIC_ESTIMATE_OVERHEAD_BYTES
247 })
248 .sum();
249 if estimated <= MAX_DIAGNOSTICS_ENTRY_BYTES {
250 return;
251 }
252 }
253
254 if fits(diagnostics) {
255 return;
256 }
257
258 let original_count = diagnostics.len();
259
260 diagnostics.sort_by_key(diagnostic_severity_rank);
269 let keep = largest_fitting_prefix(diagnostics, fits).max(1);
270 diagnostics.truncate(keep);
271 if diagnostics.len() < original_count {
272 warn!(
273 "diagnostics for {} exceeded the {MAX_DIAGNOSTICS_ENTRY_BYTES}-byte cache cap; kept \
274 the {} highest-severity of {original_count} diagnostics",
275 uri.as_str(),
276 diagnostics.len(),
277 );
278 }
279
280 if diagnostics.len() == 1 && !fits(diagnostics) {
283 let diagnostic = &mut diagnostics[0];
284 let had_data = diagnostic.data.is_some();
285 diagnostic.data = None;
286 diagnostic.code_description = None;
287 diagnostic.related_information = None;
288 diagnostic.tags = None;
289 warn!(
290 "diagnostic for {} exceeded the cache cap; dropped its data/code_description/\
291 related_information/tags fields{}",
292 uri.as_str(),
293 if had_data {
294 " (a later code-action request for this diagnostic may not resolve its quick fix)"
295 } else {
296 ""
297 },
298 );
299 }
300
301 if diagnostics.len() == 1 && !fits(diagnostics) {
305 let diagnostic = &mut diagnostics[0];
306 if let Some(source) = &diagnostic.source {
307 diagnostic.source = Some(truncate_str(source, MAX_ENTRY_TEXT_BYTES));
308 }
309 if let Some(lsp_types::NumberOrString::String(code)) = &diagnostic.code {
310 diagnostic.code = Some(lsp_types::NumberOrString::String(truncate_str(
311 code,
312 MAX_ENTRY_TEXT_BYTES,
313 )));
314 }
315 }
316
317 if !fits(diagnostics) {
321 diagnostics.truncate(1);
322 if let Some(diagnostic) = diagnostics.first_mut() {
323 diagnostic.message = truncate_str(
324 &diagnostic.message,
325 DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES,
326 );
327 diagnostic.source = None;
328 diagnostic.code = None;
329 diagnostic.code_description = None;
330 diagnostic.related_information = None;
331 diagnostic.tags = None;
332 diagnostic.data = None;
333 }
334 warn!(
335 "diagnostic for {} still exceeded the cache cap after every other mitigation; \
336 truncated its message to {DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES} bytes and \
337 cleared all other fields",
338 uri.as_str(),
339 );
340 }
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct DiagnosticInfo {
346 pub uri: Uri,
348 pub version: Option<i32>,
350 pub diagnostics: Vec<LspDiagnostic>,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct LogEntry {
357 pub level: LogLevel,
359 pub message: String,
361 pub timestamp: DateTime<Utc>,
363}
364
365#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
367#[serde(rename_all = "lowercase")]
368pub enum LogLevel {
369 Error,
371 Warning,
373 Info,
375 Debug,
377}
378
379impl From<lsp_types::MessageType> for LogLevel {
380 fn from(msg_type: lsp_types::MessageType) -> Self {
381 match msg_type {
382 lsp_types::MessageType::ERROR => Self::Error,
383 lsp_types::MessageType::WARNING => Self::Warning,
384 lsp_types::MessageType::INFO => Self::Info,
385 _ => Self::Debug,
387 }
388 }
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct ServerMessage {
394 pub message_type: MessageType,
396 pub message: String,
398 pub timestamp: DateTime<Utc>,
400}
401
402#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
404#[serde(rename_all = "lowercase")]
405pub enum MessageType {
406 Error,
408 Warning,
410 Info,
412 Log,
414}
415
416impl From<lsp_types::MessageType> for MessageType {
417 fn from(msg_type: lsp_types::MessageType) -> Self {
418 match msg_type {
419 lsp_types::MessageType::ERROR => Self::Error,
420 lsp_types::MessageType::WARNING => Self::Warning,
421 lsp_types::MessageType::INFO => Self::Info,
422 _ => Self::Log,
424 }
425 }
426}
427
428#[derive(Debug)]
430pub struct NotificationCache {
431 diagnostics: HashMap<String, DiagnosticInfo>,
433 diagnostics_owners: HashMap<String, ServerId>,
436 diagnostic_order: HashMap<ServerId, BTreeMap<u64, String>>,
449 diagnostic_seq: HashMap<String, u64>,
453 next_diagnostic_seq: u64,
458 diagnostics_route_count: usize,
462 logs: VecDeque<LogEntry>,
464 messages: VecDeque<ServerMessage>,
466}
467
468impl Default for NotificationCache {
469 fn default() -> Self {
470 Self::new()
471 }
472}
473
474impl NotificationCache {
475 #[must_use]
477 pub fn new() -> Self {
478 Self {
479 diagnostics: HashMap::with_capacity(32),
480 diagnostics_owners: HashMap::with_capacity(32),
481 diagnostic_order: HashMap::new(),
482 diagnostic_seq: HashMap::with_capacity(32),
483 next_diagnostic_seq: 0,
484 diagnostics_route_count: 1,
485 logs: VecDeque::with_capacity(MAX_LOG_ENTRIES),
486 messages: VecDeque::with_capacity(MAX_SERVER_MESSAGES),
487 }
488 }
489
490 pub fn set_diagnostics_route_count(&mut self, count: usize) {
502 self.diagnostics_route_count = count.max(1);
503 }
504
505 fn per_server_budget(&self) -> usize {
514 (MAX_DIAGNOSTIC_ENTRIES / self.diagnostics_route_count.max(1)).max(1)
515 }
516
517 fn server_to_evict_from(&self, writer: &ServerId) -> Option<ServerId> {
541 let largest = self
542 .diagnostic_order
543 .iter()
544 .filter(|(_, order)| !order.is_empty())
545 .max_by_key(|(id, order)| (order.len(), id.as_str()));
546
547 let budget = self.per_server_budget();
548 if let Some((id, order)) = largest
549 && order.len() > budget
550 {
551 return Some(id.clone());
552 }
553
554 if self
555 .diagnostic_order
556 .get(writer)
557 .is_some_and(|order| !order.is_empty())
558 {
559 return Some(writer.clone());
560 }
561
562 largest.map(|(id, _)| id.clone())
563 }
564
565 pub fn store_diagnostics(
607 &mut self,
608 server_id: &ServerId,
609 uri: &Uri,
610 version: Option<i32>,
611 mut diagnostics: Vec<LspDiagnostic>,
612 ) {
613 for diagnostic in &mut diagnostics {
618 diagnostic.message = truncate_string(
619 std::mem::take(&mut diagnostic.message),
620 MAX_ENTRY_TEXT_BYTES,
621 );
622 }
623 cap_diagnostics_entry_size(uri, &mut diagnostics);
626
627 let key = uri_cache_key(uri.as_str()).into_owned();
628 let info = DiagnosticInfo {
629 uri: uri.clone(),
630 version,
631 diagnostics,
632 };
633
634 let mut is_new_entry = true;
641 if let Some(old_seq) = self.diagnostic_seq.remove(&key) {
642 is_new_entry = false;
643 if let Some(previous_owner) = self.diagnostics_owners.get(&key)
644 && let Some(order) = self.diagnostic_order.get_mut(previous_owner)
645 {
646 order.remove(&old_seq);
647 }
648 }
649
650 if is_new_entry {
651 while self.diagnostics.len() >= MAX_DIAGNOSTIC_ENTRIES
652 && let Some(evict_from) = self.server_to_evict_from(server_id)
653 && let Some(order) = self.diagnostic_order.get_mut(&evict_from)
654 && let Some((&oldest_seq, oldest_key)) = order.iter().next()
655 {
656 let oldest_key = oldest_key.clone();
657 order.remove(&oldest_seq);
658 self.diagnostic_seq.remove(&oldest_key);
659 self.diagnostics_owners.remove(&oldest_key);
660 self.diagnostics.remove(&oldest_key);
661 }
662 }
663
664 self.diagnostics_owners
665 .insert(key.clone(), server_id.clone());
666 let seq = self.next_diagnostic_seq;
667 self.next_diagnostic_seq += 1;
668 self.diagnostic_order
669 .entry(server_id.clone())
670 .or_default()
671 .insert(seq, key.clone());
672 self.diagnostic_seq.insert(key.clone(), seq);
673 self.diagnostics.insert(key, info);
674 }
675
676 pub fn store_log(&mut self, level: LogLevel, message: String) {
681 let entry = LogEntry {
682 level,
683 message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
684 timestamp: Utc::now(),
685 };
686
687 if self.logs.len() >= MAX_LOG_ENTRIES {
688 self.logs.pop_front();
689 }
690 self.logs.push_back(entry);
691 }
692
693 pub fn store_message(&mut self, message_type: MessageType, message: String) {
698 let msg = ServerMessage {
699 message_type,
700 message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
701 timestamp: Utc::now(),
702 };
703
704 if self.messages.len() >= MAX_SERVER_MESSAGES {
705 self.messages.pop_front();
706 }
707 self.messages.push_back(msg);
708 }
709
710 #[inline]
718 #[must_use]
719 pub fn get_diagnostics(&self, uri: &str) -> Option<&DiagnosticInfo> {
720 self.diagnostics.get(uri_cache_key(uri).as_ref())
721 }
722
723 #[inline]
728 #[must_use]
729 pub fn diagnostics_owner(&self, uri: &str) -> Option<&ServerId> {
730 self.diagnostics_owners.get(uri_cache_key(uri).as_ref())
731 }
732
733 #[inline]
735 #[must_use]
736 pub const fn logs(&self) -> &VecDeque<LogEntry> {
737 &self.logs
738 }
739
740 #[inline]
742 #[must_use]
743 pub const fn messages(&self) -> &VecDeque<ServerMessage> {
744 &self.messages
745 }
746
747 pub fn clear_diagnostics(&mut self, uri: &str) -> Option<DiagnosticInfo> {
751 let key = uri_cache_key(uri).into_owned();
752 if let Some(owner) = self.diagnostics_owners.remove(&key)
753 && let Some(seq) = self.diagnostic_seq.remove(&key)
754 && let Some(order) = self.diagnostic_order.get_mut(&owner)
755 {
756 order.remove(&seq);
757 }
758 self.diagnostics.remove(&key)
759 }
760
761 pub fn clear_server_diagnostics(&mut self, server_id: &ServerId) {
788 let Some(order) = self.diagnostic_order.remove(server_id) else {
789 return;
790 };
791 for (_, key) in order {
792 self.diagnostics.remove(&key);
793 self.diagnostics_owners.remove(&key);
794 self.diagnostic_seq.remove(&key);
795 }
796 }
797
798 pub fn clear_all_diagnostics(&mut self) {
800 self.diagnostics.clear();
801 self.diagnostics_owners.clear();
802 self.diagnostic_order.clear();
803 self.diagnostic_seq.clear();
804 }
805
806 pub fn clear_logs(&mut self) {
808 self.logs.clear();
809 }
810
811 pub fn clear_messages(&mut self) {
813 self.messages.clear();
814 }
815
816 #[inline]
818 #[must_use]
819 pub fn diagnostics_count(&self) -> usize {
820 self.diagnostics.len()
821 }
822
823 #[inline]
825 #[must_use]
826 pub fn logs_count(&self) -> usize {
827 self.logs.len()
828 }
829
830 #[inline]
832 #[must_use]
833 pub fn messages_count(&self) -> usize {
834 self.messages.len()
835 }
836}
837
838#[cfg(test)]
839#[allow(clippy::unwrap_used)]
840mod tests {
841 use lsp_types::{Position, Range};
842
843 use super::*;
844
845 fn test_server() -> ServerId {
849 ServerId::from("test-server")
850 }
851
852 #[test]
853 fn test_notification_cache_new() {
854 let cache = NotificationCache::new();
855 assert_eq!(cache.diagnostics_count(), 0);
856 assert_eq!(cache.logs_count(), 0);
857 assert_eq!(cache.messages_count(), 0);
858 }
859
860 #[test]
861 fn test_store_and_get_diagnostics() {
862 let mut cache = NotificationCache::new();
863 let uri: Uri = "file:///test.rs".parse().unwrap();
864
865 let diagnostic = LspDiagnostic {
866 range: Range {
867 start: Position {
868 line: 0,
869 character: 0,
870 },
871 end: Position {
872 line: 0,
873 character: 5,
874 },
875 },
876 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
877 message: "test error".to_string(),
878 code: None,
879 source: None,
880 code_description: None,
881 related_information: None,
882 tags: None,
883 data: None,
884 };
885
886 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
887
888 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
889 assert_eq!(stored.uri, uri);
890 assert_eq!(stored.version, Some(1));
891 assert_eq!(stored.diagnostics.len(), 1);
892 assert_eq!(stored.diagnostics[0].message, "test error");
893 }
894
895 #[test]
898 fn test_store_diagnostics_truncates_oversized_message() {
899 let mut cache = NotificationCache::new();
900 let uri: Uri = "file:///test.rs".parse().unwrap();
901 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
902
903 let diagnostic = LspDiagnostic {
904 range: Range {
905 start: Position {
906 line: 0,
907 character: 0,
908 },
909 end: Position {
910 line: 0,
911 character: 5,
912 },
913 },
914 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
915 message: oversized.clone(),
916 code: None,
917 source: None,
918 code_description: None,
919 related_information: None,
920 tags: None,
921 data: None,
922 };
923
924 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
925
926 let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics[0].message;
927 assert!(stored.len() < oversized.len());
928 assert!(stored.ends_with("... (truncated)"));
929 }
930
931 fn minimal_diagnostic(message: String) -> LspDiagnostic {
934 LspDiagnostic {
935 range: Range {
936 start: Position {
937 line: 0,
938 character: 0,
939 },
940 end: Position {
941 line: 0,
942 character: 5,
943 },
944 },
945 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
946 message,
947 code: None,
948 source: None,
949 code_description: None,
950 related_information: None,
951 tags: None,
952 data: None,
953 }
954 }
955
956 #[test]
960 fn test_store_diagnostics_caps_aggregate_size_for_many_small_diagnostics() {
961 let mut cache = NotificationCache::new();
962 let uri: Uri = "file:///test.rs".parse().unwrap();
963
964 let diagnostics: Vec<LspDiagnostic> = (0..5000)
968 .map(|i| {
969 minimal_diagnostic(format!(
970 "diagnostic number {i}, padded: {}",
971 "x".repeat(200)
972 ))
973 })
974 .collect();
975 let original_count = diagnostics.len();
976
977 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
978
979 let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
980 assert!(
981 stored.len() < original_count,
982 "aggregate cap must trim the list, kept {} of {original_count}",
983 stored.len()
984 );
985 assert!(!stored.is_empty(), "must keep at least one diagnostic");
986 let serialized_len = serde_json::to_vec(stored).unwrap().len();
987 assert!(
988 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
989 "stored entry must fit the aggregate cap, got {serialized_len} bytes"
990 );
991 }
992
993 #[test]
997 fn test_store_diagnostics_truncation_keeps_largest_fitting_prefix() {
998 let mut cache = NotificationCache::new();
999 let uri: Uri = "file:///test.rs".parse().unwrap();
1000
1001 let diagnostics: Vec<LspDiagnostic> = (0..5000)
1005 .map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
1006 .collect();
1007
1008 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1009
1010 let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
1011 assert!(
1012 stored.len() > 2600,
1013 "largest-fitting-prefix search must keep far more than half, kept {}",
1014 stored.len()
1015 );
1016 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1017 assert!(serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES);
1018 let mut with_one_more = stored.clone();
1022 with_one_more.push(minimal_diagnostic(format!(
1023 "diagnostic overflow: {}",
1024 "x".repeat(250)
1025 )));
1026 assert!(
1027 serde_json::to_vec(&with_one_more).unwrap().len() > MAX_DIAGNOSTICS_ENTRY_BYTES,
1028 "kept count must be the largest that fits, not merely a fitting count"
1029 );
1030 }
1031
1032 #[test]
1036 fn test_store_diagnostics_truncation_prefers_higher_severity() {
1037 let mut cache = NotificationCache::new();
1038 let uri: Uri = "file:///test.rs".parse().unwrap();
1039
1040 let mut diagnostics: Vec<LspDiagnostic> = (0..5000)
1041 .map(|i| {
1042 let mut d = minimal_diagnostic(format!("hint {i}: {}", "x".repeat(200)));
1043 d.severity = Some(lsp_types::DiagnosticSeverity::HINT);
1044 d
1045 })
1046 .collect();
1047 let mut trailing_error = minimal_diagnostic("the one real error".to_string());
1048 trailing_error.severity = Some(lsp_types::DiagnosticSeverity::ERROR);
1049 diagnostics.push(trailing_error);
1050
1051 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1052
1053 let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
1054 assert!(
1055 stored.iter().any(|d| d.message == "the one real error"),
1056 "the trailing ERROR diagnostic must survive truncation over leading HINT noise"
1057 );
1058 }
1059
1060 #[derive(Clone, Default)]
1064 struct CapturedMessages(std::sync::Arc<std::sync::Mutex<Vec<String>>>);
1065
1066 impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for CapturedMessages {
1067 fn on_event(
1068 &self,
1069 event: &tracing::Event<'_>,
1070 _ctx: tracing_subscriber::layer::Context<'_, S>,
1071 ) {
1072 struct MessageVisitor(String);
1073 impl tracing::field::Visit for MessageVisitor {
1074 fn record_debug(
1075 &mut self,
1076 field: &tracing::field::Field,
1077 value: &dyn std::fmt::Debug,
1078 ) {
1079 if field.name() == "message" {
1080 self.0 = format!("{value:?}");
1081 }
1082 }
1083 }
1084 let mut visitor = MessageVisitor(String::new());
1085 event.record(&mut visitor);
1086 self.0.lock().unwrap().push(visitor.0);
1087 }
1088 }
1089
1090 #[test]
1094 fn test_store_diagnostics_warns_when_truncating_list() {
1095 use tracing_subscriber::layer::SubscriberExt as _;
1096
1097 let mut cache = NotificationCache::new();
1098 let uri: Uri = "file:///test.rs".parse().unwrap();
1099 let diagnostics: Vec<LspDiagnostic> = (0..5000)
1100 .map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
1101 .collect();
1102
1103 let captured = CapturedMessages::default();
1104 let subscriber = tracing_subscriber::registry().with(captured.clone());
1105 let guard = tracing::subscriber::set_default(subscriber);
1106 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1107 drop(guard);
1108
1109 let messages = captured.0.lock().unwrap().clone();
1110 assert!(
1111 messages
1112 .iter()
1113 .any(|m| m.contains("highest-severity") && m.contains("file:///test.rs")),
1114 "expected a truncation warning naming the URI, got: {messages:?}"
1115 );
1116 }
1117
1118 #[test]
1122 fn test_store_diagnostics_warns_when_dropping_data_blob() {
1123 use tracing_subscriber::layer::SubscriberExt as _;
1124
1125 let mut cache = NotificationCache::new();
1126 let uri: Uri = "file:///test.rs".parse().unwrap();
1127 let mut diagnostic = minimal_diagnostic("small message".to_string());
1128 diagnostic.data = Some(serde_json::json!({
1129 "blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
1130 }));
1131
1132 let captured = CapturedMessages::default();
1133 let subscriber = tracing_subscriber::registry().with(captured.clone());
1134 let guard = tracing::subscriber::set_default(subscriber);
1135 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1136 drop(guard);
1137
1138 let messages = captured.0.lock().unwrap().clone();
1139 assert!(
1140 messages.iter().any(|m| m.contains("code-action")),
1141 "expected a warning noting the code-action quick-fix impact, got: {messages:?}"
1142 );
1143 }
1144
1145 #[test]
1150 fn test_store_diagnostics_drops_oversized_data_blob_on_single_diagnostic() {
1151 let mut cache = NotificationCache::new();
1152 let uri: Uri = "file:///test.rs".parse().unwrap();
1153
1154 let mut diagnostic = minimal_diagnostic("small message".to_string());
1155 diagnostic.data = Some(serde_json::json!({
1156 "blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
1157 }));
1158
1159 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1160
1161 let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
1162 assert_eq!(stored.len(), 1);
1163 assert_eq!(stored[0].message, "small message");
1164 assert!(
1165 stored[0].data.is_none(),
1166 "oversized data blob must be dropped"
1167 );
1168 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1169 assert!(
1170 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1171 "stored entry must fit the aggregate cap after dropping data, got {serialized_len} bytes"
1172 );
1173 }
1174
1175 #[test]
1180 fn test_store_diagnostics_truncates_oversized_source_on_single_diagnostic() {
1181 let mut cache = NotificationCache::new();
1182 let uri: Uri = "file:///test.rs".parse().unwrap();
1183
1184 let mut diagnostic = minimal_diagnostic("small message".to_string());
1185 diagnostic.source = Some("x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000));
1186
1187 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1188
1189 let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
1190 assert_eq!(stored.len(), 1);
1191 assert_eq!(stored[0].message, "small message");
1192 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1193 assert!(
1194 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1195 "stored entry must fit the aggregate cap after truncating source, got {serialized_len} bytes"
1196 );
1197 }
1198
1199 #[test]
1206 fn test_store_diagnostics_caps_single_diagnostic_with_every_field_maxed_out() {
1207 let mut cache = NotificationCache::new();
1208 let uri: Uri = "file:///test.rs".parse().unwrap();
1209
1210 let mut diagnostic = minimal_diagnostic("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
1215 diagnostic.source = Some("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
1216 diagnostic.code = Some(lsp_types::NumberOrString::String(
1217 "x".repeat(MAX_ENTRY_TEXT_BYTES + 1000),
1218 ));
1219 diagnostic.data = Some(serde_json::json!({ "blob": "x".repeat(MAX_ENTRY_TEXT_BYTES) }));
1220 diagnostic.tags = Some(vec![lsp_types::DiagnosticTag::UNNECESSARY; 50]);
1221 diagnostic.related_information = Some(vec![
1222 lsp_types::DiagnosticRelatedInformation {
1223 location: lsp_types::Location {
1224 uri: uri.clone(),
1225 range: Range::default(),
1226 },
1227 message: "x".repeat(1000),
1228 };
1229 5
1230 ]);
1231
1232 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1233
1234 let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
1235 assert_eq!(stored.len(), 1);
1236 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1237 assert!(
1238 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1239 "postcondition must hold even with every field maxed out, got {serialized_len} bytes"
1240 );
1241 }
1242
1243 #[test]
1252 fn test_cap_diagnostics_entry_size_terminal_fallback_bounds_untruncated_message() {
1253 let uri: Uri = "file:///test.rs".parse().unwrap();
1254 let mut diagnostics = vec![minimal_diagnostic(
1255 "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
1256 )];
1257
1258 cap_diagnostics_entry_size(&uri, &mut diagnostics);
1259
1260 assert_eq!(diagnostics.len(), 1);
1261 assert!(
1262 diagnostics[0].message.len() <= DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES + 20,
1263 "terminal fallback must truncate the message itself, got {} bytes",
1264 diagnostics[0].message.len()
1265 );
1266 let serialized_len = serde_json::to_vec(&diagnostics).unwrap().len();
1267 assert!(
1268 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1269 "postcondition must hold via the terminal fallback, got {serialized_len} bytes"
1270 );
1271 }
1272
1273 #[test]
1278 fn test_store_diagnostics_cheap_path_leaves_small_diagnostics_untouched() {
1279 let mut cache = NotificationCache::new();
1280 let uri: Uri = "file:///test.rs".parse().unwrap();
1281
1282 let mut diagnostic = minimal_diagnostic("a small, ordinary diagnostic message".to_string());
1283 diagnostic.source = Some("rustc".to_string());
1284
1285 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1286
1287 let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
1288 assert_eq!(stored.len(), 1);
1289 assert_eq!(stored[0].message, "a small, ordinary diagnostic message");
1290 assert_eq!(stored[0].source.as_deref(), Some("rustc"));
1291 }
1292
1293 #[test]
1303 fn test_store_diagnostics_cheap_path_escape_safe_for_control_character_heavy_message() {
1304 let mut cache = NotificationCache::new();
1305 let uri: Uri = "file:///test.rs".parse().unwrap();
1306
1307 let nul_heavy_message = "\0".repeat(MAX_ENTRY_TEXT_BYTES);
1308 let diagnostics: Vec<LspDiagnostic> = (0..3)
1309 .map(|_| minimal_diagnostic(nul_heavy_message.clone()))
1310 .collect();
1311
1312 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1313
1314 let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
1315 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1316 assert!(
1317 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1318 "escape-heavy content must not let the cheap-estimate fast path skip the real cap, \
1319 got {serialized_len} bytes"
1320 );
1321 }
1322
1323 #[test]
1324 fn test_store_diagnostics_replaces_existing() {
1325 let mut cache = NotificationCache::new();
1326 let uri: Uri = "file:///test.rs".parse().unwrap();
1327
1328 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1329 assert_eq!(cache.diagnostics_count(), 1);
1330
1331 cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
1332 assert_eq!(cache.diagnostics_count(), 1);
1333
1334 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
1335 assert_eq!(stored.version, Some(2));
1336 }
1337
1338 #[test]
1339 fn test_clear_diagnostics() {
1340 let mut cache = NotificationCache::new();
1341 let uri: Uri = "file:///test.rs".parse().unwrap();
1342
1343 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1344 assert_eq!(cache.diagnostics_count(), 1);
1345
1346 let cleared = cache.clear_diagnostics(uri.as_str());
1347 assert!(cleared.is_some());
1348 assert_eq!(cache.diagnostics_count(), 0);
1349 }
1350
1351 #[test]
1352 fn test_clear_all_diagnostics() {
1353 let mut cache = NotificationCache::new();
1354 let uri1: Uri = "file:///test1.rs".parse().unwrap();
1355 let uri2: Uri = "file:///test2.rs".parse().unwrap();
1356
1357 cache.store_diagnostics(&test_server(), &uri1, Some(1), vec![]);
1358 cache.store_diagnostics(&test_server(), &uri2, Some(1), vec![]);
1359 assert_eq!(cache.diagnostics_count(), 2);
1360
1361 cache.clear_all_diagnostics();
1362 assert_eq!(cache.diagnostics_count(), 0);
1363 }
1364
1365 #[test]
1366 fn test_store_and_get_logs() {
1367 let mut cache = NotificationCache::new();
1368
1369 cache.store_log(LogLevel::Error, "error message".to_string());
1370 cache.store_log(LogLevel::Info, "info message".to_string());
1371
1372 let logs = cache.logs();
1373 assert_eq!(logs.len(), 2);
1374 assert_eq!(logs[0].level, LogLevel::Error);
1375 assert_eq!(logs[0].message, "error message");
1376 assert_eq!(logs[1].level, LogLevel::Info);
1377 assert_eq!(logs[1].message, "info message");
1378 }
1379
1380 #[test]
1381 fn test_logs_max_capacity() {
1382 let mut cache = NotificationCache::new();
1383
1384 for i in 0..MAX_LOG_ENTRIES + 10 {
1386 cache.store_log(LogLevel::Info, format!("message {i}"));
1387 }
1388
1389 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
1390
1391 let logs = cache.logs();
1393 assert_eq!(logs.front().unwrap().message, "message 10");
1394 assert_eq!(
1395 logs.back().unwrap().message,
1396 format!("message {}", MAX_LOG_ENTRIES + 9)
1397 );
1398 }
1399
1400 #[test]
1404 fn test_store_log_truncates_oversized_message() {
1405 let mut cache = NotificationCache::new();
1406 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
1407
1408 cache.store_log(LogLevel::Info, oversized.clone());
1409
1410 let stored = &cache.logs()[0].message;
1411 assert!(stored.len() < oversized.len());
1412 assert!(stored.ends_with("... (truncated)"));
1413 }
1414
1415 #[test]
1416 fn test_store_log_does_not_truncate_message_at_or_below_limit() {
1417 let mut cache = NotificationCache::new();
1418 let message = "a".repeat(MAX_ENTRY_TEXT_BYTES);
1419
1420 cache.store_log(LogLevel::Info, message.clone());
1421
1422 assert_eq!(cache.logs()[0].message, message);
1423 }
1424
1425 #[test]
1426 fn test_clear_logs() {
1427 let mut cache = NotificationCache::new();
1428 cache.store_log(LogLevel::Info, "test".to_string());
1429 assert_eq!(cache.logs_count(), 1);
1430
1431 cache.clear_logs();
1432 assert_eq!(cache.logs_count(), 0);
1433 }
1434
1435 #[test]
1436 fn test_store_and_get_messages() {
1437 let mut cache = NotificationCache::new();
1438
1439 cache.store_message(MessageType::Error, "error msg".to_string());
1440 cache.store_message(MessageType::Warning, "warning msg".to_string());
1441
1442 let messages = cache.messages();
1443 assert_eq!(messages.len(), 2);
1444 assert_eq!(messages[0].message_type, MessageType::Error);
1445 assert_eq!(messages[0].message, "error msg");
1446 assert_eq!(messages[1].message_type, MessageType::Warning);
1447 assert_eq!(messages[1].message, "warning msg");
1448 }
1449
1450 #[test]
1451 fn test_messages_max_capacity() {
1452 let mut cache = NotificationCache::new();
1453
1454 for i in 0..MAX_SERVER_MESSAGES + 10 {
1456 cache.store_message(MessageType::Info, format!("message {i}"));
1457 }
1458
1459 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
1460
1461 let messages = cache.messages();
1463 assert_eq!(messages.front().unwrap().message, "message 10");
1464 assert_eq!(
1465 messages.back().unwrap().message,
1466 format!("message {}", MAX_SERVER_MESSAGES + 9)
1467 );
1468 }
1469
1470 #[test]
1471 fn test_clear_messages() {
1472 let mut cache = NotificationCache::new();
1473 cache.store_message(MessageType::Info, "test".to_string());
1474 assert_eq!(cache.messages_count(), 1);
1475
1476 cache.clear_messages();
1477 assert_eq!(cache.messages_count(), 0);
1478 }
1479
1480 #[test]
1482 fn test_store_message_truncates_oversized_message() {
1483 let mut cache = NotificationCache::new();
1484 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
1485
1486 cache.store_message(MessageType::Info, oversized.clone());
1487
1488 let stored = &cache.messages()[0].message;
1489 assert!(stored.len() < oversized.len());
1490 assert!(stored.ends_with("... (truncated)"));
1491 }
1492
1493 #[test]
1494 fn test_log_levels() {
1495 let mut cache = NotificationCache::new();
1496
1497 cache.store_log(LogLevel::Error, "error".to_string());
1498 cache.store_log(LogLevel::Warning, "warning".to_string());
1499 cache.store_log(LogLevel::Info, "info".to_string());
1500 cache.store_log(LogLevel::Debug, "debug".to_string());
1501
1502 let logs = cache.logs();
1503 assert_eq!(logs[0].level, LogLevel::Error);
1504 assert_eq!(logs[1].level, LogLevel::Warning);
1505 assert_eq!(logs[2].level, LogLevel::Info);
1506 assert_eq!(logs[3].level, LogLevel::Debug);
1507 }
1508
1509 #[test]
1510 fn test_message_types() {
1511 let mut cache = NotificationCache::new();
1512
1513 cache.store_message(MessageType::Error, "error".to_string());
1514 cache.store_message(MessageType::Warning, "warning".to_string());
1515 cache.store_message(MessageType::Info, "info".to_string());
1516 cache.store_message(MessageType::Log, "log".to_string());
1517
1518 let messages = cache.messages();
1519 assert_eq!(messages[0].message_type, MessageType::Error);
1520 assert_eq!(messages[1].message_type, MessageType::Warning);
1521 assert_eq!(messages[2].message_type, MessageType::Info);
1522 assert_eq!(messages[3].message_type, MessageType::Log);
1523 }
1524
1525 #[test]
1526 fn test_timestamp_ordering() {
1527 let mut cache = NotificationCache::new();
1528
1529 cache.store_log(LogLevel::Info, "first".to_string());
1530 std::thread::sleep(std::time::Duration::from_millis(10));
1531 cache.store_log(LogLevel::Info, "second".to_string());
1532
1533 let logs = cache.logs();
1534 assert!(logs[0].timestamp < logs[1].timestamp);
1535 }
1536
1537 #[test]
1538 fn test_store_diagnostics_empty_list() {
1539 let mut cache = NotificationCache::new();
1540 let uri: Uri = "file:///test.rs".parse().unwrap();
1541
1542 let diagnostic = LspDiagnostic {
1543 range: Range {
1544 start: Position {
1545 line: 0,
1546 character: 0,
1547 },
1548 end: Position {
1549 line: 0,
1550 character: 5,
1551 },
1552 },
1553 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
1554 message: "test error".to_string(),
1555 code: None,
1556 source: None,
1557 code_description: None,
1558 related_information: None,
1559 tags: None,
1560 data: None,
1561 };
1562
1563 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1564 assert_eq!(
1565 cache
1566 .get_diagnostics(uri.as_str())
1567 .unwrap()
1568 .diagnostics
1569 .len(),
1570 1
1571 );
1572
1573 cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
1574 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
1575 assert_eq!(stored.diagnostics.len(), 0);
1576 assert_eq!(stored.version, Some(2));
1577 }
1578
1579 #[test]
1580 fn test_store_many_diagnostics_single_file() {
1581 let mut cache = NotificationCache::new();
1582 let uri: Uri = "file:///test.rs".parse().unwrap();
1583
1584 let diagnostics: Vec<LspDiagnostic> = (0..100)
1585 .map(|i| LspDiagnostic {
1586 range: Range {
1587 start: Position {
1588 line: i,
1589 character: 0,
1590 },
1591 end: Position {
1592 line: i,
1593 character: 10,
1594 },
1595 },
1596 message: format!("Error {i}"),
1597 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
1598 code: None,
1599 source: None,
1600 code_description: None,
1601 related_information: None,
1602 tags: None,
1603 data: None,
1604 })
1605 .collect();
1606
1607 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1608
1609 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
1610 assert_eq!(stored.diagnostics.len(), 100);
1611 }
1612
1613 #[test]
1614 fn test_logs_exact_capacity_boundary() {
1615 let mut cache = NotificationCache::new();
1616
1617 for i in 0..MAX_LOG_ENTRIES {
1618 cache.store_log(LogLevel::Info, format!("message {i}"));
1619 }
1620 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
1621
1622 cache.store_log(LogLevel::Info, "overflow".to_string());
1623 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
1624 assert_eq!(cache.logs().front().unwrap().message, "message 1");
1625 }
1626
1627 #[test]
1628 fn test_messages_exact_capacity_boundary() {
1629 let mut cache = NotificationCache::new();
1630
1631 for i in 0..MAX_SERVER_MESSAGES {
1632 cache.store_message(MessageType::Info, format!("message {i}"));
1633 }
1634 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
1635
1636 cache.store_message(MessageType::Info, "overflow".to_string());
1637 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
1638 assert_eq!(cache.messages().front().unwrap().message, "message 1");
1639 }
1640
1641 #[test]
1642 fn test_diagnostics_max_capacity() {
1643 let mut cache = NotificationCache::new();
1644
1645 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 10 {
1646 let uri: Uri = format!("file:///test{i}.rs").parse().unwrap();
1647 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1648 }
1649
1650 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1651
1652 let evicted: Uri = "file:///test0.rs".parse().unwrap();
1654 assert!(cache.get_diagnostics(evicted.as_str()).is_none());
1655 let newest: Uri = format!("file:///test{}.rs", MAX_DIAGNOSTIC_ENTRIES + 9)
1656 .parse()
1657 .unwrap();
1658 assert!(cache.get_diagnostics(newest.as_str()).is_some());
1659 }
1660
1661 #[test]
1662 fn test_diagnostics_replacing_existing_uri_does_not_trigger_eviction() {
1663 let mut cache = NotificationCache::new();
1664 let uri: Uri = "file:///stable.rs".parse().unwrap();
1665
1666 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
1667 cache.store_diagnostics(
1668 &test_server(),
1669 &uri,
1670 Some(i32::try_from(i).unwrap()),
1671 vec![],
1672 );
1673 }
1674 assert_eq!(cache.diagnostics_count(), 1);
1675 assert!(cache.get_diagnostics(uri.as_str()).is_some());
1676 }
1677
1678 #[test]
1679 fn test_diagnostics_republish_refreshes_eviction_order() {
1680 let mut cache = NotificationCache::new();
1684 let actively_edited: Uri = "file:///keep.rs".parse().unwrap();
1685 cache.store_diagnostics(&test_server(), &actively_edited, Some(1), vec![]);
1686
1687 for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
1689 let uri: Uri = format!("file:///untouched{i}.rs").parse().unwrap();
1690 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1691 }
1692 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1693
1694 cache.store_diagnostics(&test_server(), &actively_edited, Some(2), vec![]);
1698
1699 let overflow: Uri = "file:///overflow.rs".parse().unwrap();
1702 cache.store_diagnostics(&test_server(), &overflow, Some(1), vec![]);
1703
1704 assert!(
1705 cache.get_diagnostics(actively_edited.as_str()).is_some(),
1706 "republished entry must survive eviction after being refreshed"
1707 );
1708 let oldest_untouched: Uri = "file:///untouched0.rs".parse().unwrap();
1709 assert!(
1710 cache.get_diagnostics(oldest_untouched.as_str()).is_none(),
1711 "the oldest never-republished entry must be evicted instead"
1712 );
1713 assert!(cache.get_diagnostics(overflow.as_str()).is_some());
1714 }
1715
1716 #[test]
1717 fn test_clear_diagnostics_then_refill_does_not_evict_early() {
1718 let mut cache = NotificationCache::new();
1719 let first: Uri = "file:///first.rs".parse().unwrap();
1720 cache.store_diagnostics(&test_server(), &first, Some(1), vec![]);
1721 cache.clear_diagnostics(first.as_str());
1722 assert_eq!(cache.diagnostics_count(), 0);
1723
1724 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
1725 let uri: Uri = format!("file:///test{i}.rs").parse().unwrap();
1726 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1727 }
1728 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1729 let first_of_batch: Uri = "file:///test0.rs".parse().unwrap();
1733 assert!(cache.get_diagnostics(first_of_batch.as_str()).is_some());
1734 }
1735
1736 #[test]
1737 fn test_clear_diagnostics_nonexistent() {
1738 let mut cache = NotificationCache::new();
1739 let result = cache.clear_diagnostics("file:///nonexistent.rs");
1740 assert!(result.is_none());
1741 }
1742
1743 #[test]
1744 fn test_store_diagnostics_no_version() {
1745 let mut cache = NotificationCache::new();
1746 let uri: Uri = "file:///test.rs".parse().unwrap();
1747
1748 cache.store_diagnostics(&test_server(), &uri, None, vec![]);
1749 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
1750 assert_eq!(stored.version, None);
1751 }
1752
1753 #[test]
1759 fn test_noisy_server_does_not_evict_quiet_server_entries() {
1760 let mut cache = NotificationCache::new();
1761 cache.set_diagnostics_route_count(2);
1762 let noisy = ServerId::from("noisy");
1763 let quiet = ServerId::from("quiet");
1764
1765 let quiet_uri: Uri = "file:///quiet/only_file.rs".parse().unwrap();
1766 cache.store_diagnostics(&quiet, &quiet_uri, Some(1), vec![]);
1767
1768 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 50 {
1773 let uri: Uri = format!("file:///noisy/file{i}.rs").parse().unwrap();
1774 cache.store_diagnostics(&noisy, &uri, Some(1), vec![]);
1775 }
1776
1777 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1778 assert!(
1779 cache.get_diagnostics(quiet_uri.as_str()).is_some(),
1780 "quiet server's only entry must survive the noisy server's overflow"
1781 );
1782
1783 let noisy_first: Uri = "file:///noisy/file0.rs".parse().unwrap();
1784 assert!(
1785 cache.get_diagnostics(noisy_first.as_str()).is_none(),
1786 "noisy server's own oldest entries must be evicted once the aggregate cache is full"
1787 );
1788 }
1789
1790 #[test]
1796 fn test_dominant_server_exceeds_equal_share_while_others_idle() {
1797 let mut cache = NotificationCache::new();
1798 cache.set_diagnostics_route_count(4);
1799 let dominant = ServerId::from("dominant");
1800
1801 let equal_share = MAX_DIAGNOSTIC_ENTRIES / 4;
1802 let more_than_share = equal_share + 100;
1803 for i in 0..more_than_share {
1804 let uri: Uri = format!("file:///file{i}.rs").parse().unwrap();
1805 cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
1806 }
1807 assert_eq!(
1808 cache.diagnostics_count(),
1809 more_than_share,
1810 "a dominant server must be able to exceed its static equal share while the aggregate has room"
1811 );
1812
1813 for i in more_than_share..MAX_DIAGNOSTIC_ENTRIES {
1816 let uri: Uri = format!("file:///file{i}.rs").parse().unwrap();
1817 cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
1818 }
1819 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1820 }
1821
1822 #[test]
1835 fn test_eviction_target_tie_break_is_deterministic() {
1836 let mut cache = NotificationCache::new();
1837 cache.set_diagnostics_route_count(1000); let a = ServerId::from("a");
1840 let b = ServerId::from("b");
1841 for i in 0..2 {
1842 let uri: Uri = format!("file:///a/file{i}.rs").parse().unwrap();
1843 cache.store_diagnostics(&a, &uri, Some(1), vec![]);
1844 }
1845 for i in 0..2 {
1846 let uri: Uri = format!("file:///b/file{i}.rs").parse().unwrap();
1847 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
1848 }
1849
1850 let writer = ServerId::from("writer");
1854 assert_eq!(cache.server_to_evict_from(&writer), Some(b));
1855 }
1856
1857 #[test]
1866 fn test_new_writer_still_evicts_when_every_existing_server_is_in_share() {
1867 let mut cache = NotificationCache::new();
1868 cache.set_diagnostics_route_count(2); let a = ServerId::from("a");
1871 let b = ServerId::from("b");
1872 for i in 0..500 {
1873 let uri: Uri = format!("file:///a/file{i}.rs").parse().unwrap();
1874 cache.store_diagnostics(&a, &uri, Some(1), vec![]);
1875 }
1876 for i in 0..500 {
1877 let uri: Uri = format!("file:///b/file{i}.rs").parse().unwrap();
1878 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
1879 }
1880 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1881
1882 let c = ServerId::from("c");
1885 let new_uri: Uri = "file:///c/first.rs".parse().unwrap();
1886 cache.store_diagnostics(&c, &new_uri, Some(1), vec![]);
1887
1888 assert_eq!(
1889 cache.diagnostics_count(),
1890 MAX_DIAGNOSTIC_ENTRIES,
1891 "the aggregate cap must still be enforced even when every existing server is within share"
1892 );
1893 assert!(cache.get_diagnostics(new_uri.as_str()).is_some());
1894
1895 let b_oldest: Uri = "file:///b/file0.rs".parse().unwrap();
1899 assert!(
1900 cache.get_diagnostics(b_oldest.as_str()).is_none(),
1901 "the largest in-share server (tie-broken to b) must lose its oldest entry"
1902 );
1903 assert!(
1904 cache.get_diagnostics("file:///a/file0.rs").is_some(),
1905 "the other in-share server must be untouched"
1906 );
1907 }
1908
1909 #[test]
1912 fn test_repeated_writes_same_owner_do_not_grow_order_map() {
1913 let mut cache = NotificationCache::new();
1914 let server = ServerId::from("server");
1915 let uri: Uri = "file:///test.rs".parse().unwrap();
1916
1917 let max_version = i32::try_from(MAX_DIAGNOSTIC_ENTRIES).unwrap() + 10;
1918 for version in 0..max_version {
1919 cache.store_diagnostics(&server, &uri, Some(version), vec![]);
1920 }
1921
1922 assert_eq!(cache.diagnostics_count(), 1);
1923 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
1924 assert_eq!(stored.version, Some(max_version - 1));
1925 }
1926
1927 #[test]
1931 fn test_store_diagnostics_reassigns_ownership() {
1932 let mut cache = NotificationCache::new();
1933 let old_owner = ServerId::from("old");
1934 let new_owner = ServerId::from("new");
1935 let uri: Uri = "file:///test.rs".parse().unwrap();
1936
1937 cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
1938 cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
1939
1940 assert_eq!(cache.diagnostics_count(), 1);
1941 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
1942 assert_eq!(stored.version, Some(2));
1943
1944 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 5 {
1948 let other: Uri = format!("file:///old/file{i}.rs").parse().unwrap();
1949 cache.store_diagnostics(&old_owner, &other, Some(1), vec![]);
1950 }
1951 assert!(cache.get_diagnostics(uri.as_str()).is_some());
1952 }
1953
1954 #[test]
1959 fn test_diagnostics_owner_returns_publisher_after_store() {
1960 let mut cache = NotificationCache::new();
1961 let server = ServerId::from("rust");
1962 let uri: Uri = "file:///main.rs".parse().unwrap();
1963
1964 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
1965
1966 assert_eq!(cache.diagnostics_owner(uri.as_str()), Some(&server));
1967 }
1968
1969 #[test]
1970 fn test_diagnostics_owner_none_for_untracked_uri() {
1971 let cache = NotificationCache::new();
1972 let uri: Uri = "file:///never-seen.rs".parse().unwrap();
1973
1974 assert_eq!(cache.diagnostics_owner(uri.as_str()), None);
1975 }
1976
1977 #[test]
1982 fn test_diagnostics_owner_reflects_reassigned_ownership() {
1983 let mut cache = NotificationCache::new();
1984 let old_owner = ServerId::from("old");
1985 let new_owner = ServerId::from("new");
1986 let uri: Uri = "file:///test.rs".parse().unwrap();
1987
1988 cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
1989 assert_eq!(cache.diagnostics_owner(uri.as_str()), Some(&old_owner));
1990
1991 cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
1992 assert_eq!(cache.diagnostics_owner(uri.as_str()), Some(&new_owner));
1993 }
1994
1995 #[test]
1998 fn test_clear_server_diagnostics_scopes_to_one_server() {
1999 let mut cache = NotificationCache::new();
2000 let crashed = ServerId::from("crashed");
2001 let healthy = ServerId::from("healthy");
2002
2003 let crashed_uri: Uri = "file:///crashed/main.py".parse().unwrap();
2004 let healthy_uri: Uri = "file:///healthy/main.rs".parse().unwrap();
2005 cache.store_diagnostics(&crashed, &crashed_uri, Some(1), vec![]);
2006 cache.store_diagnostics(&healthy, &healthy_uri, Some(1), vec![]);
2007
2008 cache.clear_server_diagnostics(&crashed);
2009
2010 assert!(cache.get_diagnostics(crashed_uri.as_str()).is_none());
2011 assert!(cache.get_diagnostics(healthy_uri.as_str()).is_some());
2012 assert_eq!(cache.diagnostics_count(), 1);
2013
2014 cache.clear_server_diagnostics(&crashed);
2016 assert_eq!(cache.diagnostics_count(), 1);
2017 }
2018
2019 #[test]
2027 fn test_shrinking_budget_affects_eviction_target_not_existing_entries() {
2028 let mut cache = NotificationCache::new();
2029 let server = ServerId::from("server");
2030
2031 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
2032 let uri: Uri = format!("file:///file{i}.rs").parse().unwrap();
2033 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
2034 }
2035 assert_eq!(
2036 cache.diagnostics_count(),
2037 MAX_DIAGNOSTIC_ENTRIES,
2038 "filling to the aggregate cap must not evict anything early"
2039 );
2040
2041 cache.set_diagnostics_route_count(4);
2044 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2045
2046 let other = ServerId::from("other");
2049 let new_uri: Uri = "file:///other/new.rs".parse().unwrap();
2050 cache.store_diagnostics(&other, &new_uri, Some(1), vec![]);
2051
2052 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2053 assert!(cache.get_diagnostics(new_uri.as_str()).is_some());
2054 let server_oldest: Uri = "file:///file0.rs".parse().unwrap();
2055 assert!(
2056 cache.get_diagnostics(server_oldest.as_str()).is_none(),
2057 "the pre-existing server's oldest entry, now far over its shrunk share, must be evicted"
2058 );
2059 }
2060}