1use std::collections::{BTreeMap, HashMap, HashSet, 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;
82
83fn uri_cache_key(uri: &str) -> std::borrow::Cow<'_, str> {
91 if cfg!(windows) {
92 std::borrow::Cow::Owned(uri.to_ascii_lowercase())
93 } else {
94 std::borrow::Cow::Borrowed(uri)
95 }
96}
97
98const MAX_SERVER_MESSAGES: usize = 50;
100
101const DIAGNOSTIC_ESTIMATE_OVERHEAD_BYTES: usize = 256;
108
109const JSON_ESCAPE_WORST_CASE_FACTOR: usize = 6;
123
124const DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES: usize = 1024;
130
131const fn diagnostic_severity_rank(diagnostic: &LspDiagnostic) -> u8 {
142 match diagnostic.severity {
143 Some(lsp_types::DiagnosticSeverity::Error) => 0,
144 Some(lsp_types::DiagnosticSeverity::Warning) => 1,
145 Some(lsp_types::DiagnosticSeverity::Information) => 2,
146 Some(_) | None => 3,
149 }
150}
151
152fn largest_fitting_prefix(
163 diagnostics: &[LspDiagnostic],
164 fits: impl Fn(&[LspDiagnostic]) -> bool,
165) -> usize {
166 let (mut lo, mut hi) = (0usize, diagnostics.len());
167 while lo < hi {
168 let mid = lo + (hi - lo).div_ceil(2);
169 if fits(&diagnostics[..mid]) {
170 lo = mid;
171 } else {
172 hi = mid - 1;
173 }
174 }
175 lo
176}
177
178pub fn message_as_str(message: &lsp_types::Message) -> &str {
182 match message {
183 lsp_types::Message::String(s) => s,
184 lsp_types::Message::MarkupContent(m) => &m.value,
185 }
186}
187
188fn truncate_message(message: lsp_types::Message, max_bytes: usize) -> lsp_types::Message {
191 match message {
192 lsp_types::Message::String(s) => lsp_types::Message::String(truncate_string(s, max_bytes)),
193 lsp_types::Message::MarkupContent(mut m) => {
194 m.value = truncate_string(m.value, max_bytes);
195 lsp_types::Message::MarkupContent(m)
196 }
197 }
198}
199
200fn cap_diagnostics_entry_size(uri: &Uri, diagnostics: &mut Vec<LspDiagnostic>) {
243 let fits = |ds: &[LspDiagnostic]| {
244 serde_json::to_vec(ds).is_ok_and(|bytes| bytes.len() <= MAX_DIAGNOSTICS_ENTRY_BYTES)
251 };
252
253 let cheaply_estimable = diagnostics.iter().all(|d| {
254 d.data.is_none()
255 && d.code_description.is_none()
256 && d.related_information.is_none()
257 && d.tags.is_none()
258 });
259 if cheaply_estimable {
260 let estimated: usize = diagnostics
261 .iter()
262 .map(|d| {
263 let raw_string_bytes = message_as_str(&d.message).len()
264 + d.source.as_deref().map_or(0, str::len)
265 + match &d.code {
266 Some(lsp_types::Code::String(s)) => s.len(),
267 _ => 0,
268 };
269 raw_string_bytes * JSON_ESCAPE_WORST_CASE_FACTOR
270 + DIAGNOSTIC_ESTIMATE_OVERHEAD_BYTES
271 })
272 .sum();
273 if estimated <= MAX_DIAGNOSTICS_ENTRY_BYTES {
274 return;
275 }
276 }
277
278 if fits(diagnostics) {
279 return;
280 }
281
282 let original_count = diagnostics.len();
283
284 diagnostics.sort_by_key(diagnostic_severity_rank);
293 let keep = largest_fitting_prefix(diagnostics, fits).max(1);
294 diagnostics.truncate(keep);
295 if diagnostics.len() < original_count {
296 warn!(
297 "diagnostics for {} exceeded the {MAX_DIAGNOSTICS_ENTRY_BYTES}-byte cache cap; kept \
298 the {} highest-severity of {original_count} diagnostics",
299 uri.as_ref(),
300 diagnostics.len(),
301 );
302 }
303
304 if diagnostics.len() == 1 && !fits(diagnostics) {
307 let diagnostic = &mut diagnostics[0];
308 let had_data = diagnostic.data.is_some();
309 diagnostic.data = None;
310 diagnostic.code_description = None;
311 diagnostic.related_information = None;
312 diagnostic.tags = None;
313 warn!(
314 "diagnostic for {} exceeded the cache cap; dropped its data/code_description/\
315 related_information/tags fields{}",
316 uri.as_ref(),
317 if had_data {
318 " (a later code-action request for this diagnostic may not resolve its quick fix)"
319 } else {
320 ""
321 },
322 );
323 }
324
325 if diagnostics.len() == 1 && !fits(diagnostics) {
329 let diagnostic = &mut diagnostics[0];
330 if let Some(source) = &diagnostic.source {
331 diagnostic.source = Some(truncate_str(source, MAX_ENTRY_TEXT_BYTES));
332 }
333 if let Some(lsp_types::Code::String(code)) = &diagnostic.code {
334 diagnostic.code = Some(lsp_types::Code::String(truncate_str(
335 code,
336 MAX_ENTRY_TEXT_BYTES,
337 )));
338 }
339 }
340
341 if !fits(diagnostics) {
345 diagnostics.truncate(1);
346 if let Some(diagnostic) = diagnostics.first_mut() {
347 let placeholder = lsp_types::Message::String(String::new());
348 diagnostic.message = truncate_message(
349 std::mem::replace(&mut diagnostic.message, placeholder),
350 DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES,
351 );
352 diagnostic.source = None;
353 diagnostic.code = None;
354 diagnostic.code_description = None;
355 diagnostic.related_information = None;
356 diagnostic.tags = None;
357 diagnostic.data = None;
358 }
359 warn!(
360 "diagnostic for {} still exceeded the cache cap after every other mitigation; \
361 truncated its message to {DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES} bytes and \
362 cleared all other fields",
363 uri.as_ref(),
364 );
365 }
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct DiagnosticInfo {
371 pub uri: Uri,
373 pub version: Option<i32>,
375 pub diagnostics: Vec<LspDiagnostic>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct LogEntry {
382 pub level: LogLevel,
384 pub message: String,
386 pub timestamp: DateTime<Utc>,
388}
389
390#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
392#[serde(rename_all = "lowercase")]
393pub enum LogLevel {
394 Error,
396 Warning,
398 Info,
400 Debug,
402}
403
404impl From<lsp_types::MessageType> for LogLevel {
405 fn from(msg_type: lsp_types::MessageType) -> Self {
406 match msg_type {
407 lsp_types::MessageType::Error => Self::Error,
408 lsp_types::MessageType::Warning => Self::Warning,
409 lsp_types::MessageType::Info => Self::Info,
410 _ => Self::Debug,
412 }
413 }
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct ServerMessage {
419 pub message_type: MessageType,
421 pub message: String,
423 pub timestamp: DateTime<Utc>,
425}
426
427#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
429#[serde(rename_all = "lowercase")]
430pub enum MessageType {
431 Error,
433 Warning,
435 Info,
437 Log,
439}
440
441impl From<lsp_types::MessageType> for MessageType {
442 fn from(msg_type: lsp_types::MessageType) -> Self {
443 match msg_type {
444 lsp_types::MessageType::Error => Self::Error,
445 lsp_types::MessageType::Warning => Self::Warning,
446 lsp_types::MessageType::Info => Self::Info,
447 _ => Self::Log,
449 }
450 }
451}
452
453#[derive(Debug)]
455pub struct NotificationCache {
456 diagnostics: HashMap<String, DiagnosticInfo>,
458 diagnostics_owners: HashMap<String, ServerId>,
461 diagnostic_order: HashMap<ServerId, BTreeMap<u64, String>>,
474 diagnostic_seq: HashMap<String, u64>,
478 next_diagnostic_seq: u64,
483 diagnostics_route_count: Option<usize>,
498
499 empty_diagnostics_count: usize,
509 logs: VecDeque<LogEntry>,
511 messages: VecDeque<ServerMessage>,
513 push_degraded: HashSet<ServerId>,
524}
525
526impl Default for NotificationCache {
527 fn default() -> Self {
528 Self::new()
529 }
530}
531
532impl NotificationCache {
533 #[must_use]
535 pub fn new() -> Self {
536 Self {
537 diagnostics: HashMap::with_capacity(32),
538 diagnostics_owners: HashMap::with_capacity(32),
539 diagnostic_order: HashMap::new(),
540 diagnostic_seq: HashMap::with_capacity(32),
541 next_diagnostic_seq: 0,
542 diagnostics_route_count: None,
543 empty_diagnostics_count: 0,
544 logs: VecDeque::with_capacity(MAX_LOG_ENTRIES),
545 messages: VecDeque::with_capacity(MAX_SERVER_MESSAGES),
546 push_degraded: HashSet::new(),
547 }
548 }
549
550 pub fn set_diagnostics_route_count(&mut self, count: usize) {
567 self.diagnostics_route_count = Some(count.max(1));
568 }
569
570 fn per_server_budget(&self) -> usize {
589 let count = self
590 .diagnostics_route_count
591 .unwrap_or_else(|| {
592 self.diagnostic_order
593 .values()
594 .filter(|order| !order.is_empty())
595 .count()
596 })
597 .max(1);
598 (MAX_DIAGNOSTIC_ENTRIES / count).max(1)
599 }
600
601 fn server_to_evict_from(&self, writer: &ServerId) -> Option<ServerId> {
625 let largest = self
626 .diagnostic_order
627 .iter()
628 .filter(|(_, order)| !order.is_empty())
629 .max_by_key(|(id, order)| (order.len(), id.as_str()));
630
631 let budget = self.per_server_budget();
632 if let Some((id, order)) = largest
633 && order.len() > budget
634 {
635 return Some(id.clone());
636 }
637
638 if self
639 .diagnostic_order
640 .get(writer)
641 .is_some_and(|order| !order.is_empty())
642 {
643 return Some(writer.clone());
644 }
645
646 largest.map(|(id, _)| id.clone())
647 }
648
649 fn is_empty_entry(&self, key: &str) -> bool {
655 self.diagnostics
656 .get(key)
657 .is_some_and(|info| info.diagnostics.is_empty())
658 }
659
660 fn oldest_empty_entry_in(&self, server: &ServerId) -> Option<(u64, String)> {
663 let order = self.diagnostic_order.get(server)?;
664 order
665 .iter()
666 .find(|(_, key)| self.is_empty_entry(key))
667 .map(|(&seq, key)| (seq, key.clone()))
668 }
669
670 fn entry_to_evict(&self, writer: &ServerId) -> Option<(ServerId, u64, String)> {
701 let evict_from = self.server_to_evict_from(writer)?;
702
703 if self.empty_diagnostics_count > 0 {
704 if let Some((seq, key)) = self.oldest_empty_entry_in(&evict_from) {
705 return Some((evict_from, seq, key));
706 }
707
708 let budget = self.per_server_budget();
709 let cross_server_pick = self
710 .diagnostic_order
711 .iter()
712 .filter(|(id, order)| order.len() > budget && *id != &evict_from)
713 .filter_map(|(id, order)| {
714 self.oldest_empty_entry_in(id)
715 .map(|(seq, key)| (id, order.len(), seq, key))
716 })
717 .max_by_key(|(id, len, ..)| (*len, id.as_str()));
718
719 if let Some((id, _, seq, key)) = cross_server_pick {
720 return Some((id.clone(), seq, key));
721 }
722 }
723
724 let order = self.diagnostic_order.get(&evict_from)?;
725 let (&seq, key) = order.iter().next()?;
726 Some((evict_from, seq, key.clone()))
727 }
728
729 pub fn store_diagnostics(
772 &mut self,
773 server_id: &ServerId,
774 uri: &Uri,
775 version: Option<i32>,
776 mut diagnostics: Vec<LspDiagnostic>,
777 ) {
778 for diagnostic in &mut diagnostics {
783 let placeholder = lsp_types::Message::String(String::new());
784 diagnostic.message = truncate_message(
785 std::mem::replace(&mut diagnostic.message, placeholder),
786 MAX_ENTRY_TEXT_BYTES,
787 );
788 }
789 cap_diagnostics_entry_size(uri, &mut diagnostics);
792
793 let key = uri_cache_key(uri.as_ref()).into_owned();
794 let info = DiagnosticInfo {
795 uri: uri.clone(),
796 version,
797 diagnostics,
798 };
799
800 let mut is_new_entry = true;
807 if let Some(old_seq) = self.diagnostic_seq.remove(&key) {
808 is_new_entry = false;
809 if let Some(previous_owner) = self.diagnostics_owners.get(&key)
810 && let Some(order) = self.diagnostic_order.get_mut(previous_owner)
811 {
812 order.remove(&old_seq);
813 }
814 }
815
816 if is_new_entry {
817 while self.diagnostics.len() >= MAX_DIAGNOSTIC_ENTRIES
818 && let Some((owner, seq, evict_key)) = self.entry_to_evict(server_id)
819 {
820 if let Some(order) = self.diagnostic_order.get_mut(&owner) {
821 order.remove(&seq);
822 }
823 self.diagnostic_seq.remove(&evict_key);
824 self.diagnostics_owners.remove(&evict_key);
825 if let Some(removed) = self.diagnostics.remove(&evict_key)
826 && removed.diagnostics.is_empty()
827 {
828 self.empty_diagnostics_count -= 1;
829 }
830 }
831 }
832
833 self.diagnostics_owners
834 .insert(key.clone(), server_id.clone());
835 let seq = self.next_diagnostic_seq;
836 self.next_diagnostic_seq += 1;
837 self.diagnostic_order
838 .entry(server_id.clone())
839 .or_default()
840 .insert(seq, key.clone());
841 self.diagnostic_seq.insert(key.clone(), seq);
842
843 let was_empty = self.is_empty_entry(&key);
849 let is_empty_now = info.diagnostics.is_empty();
850 match (was_empty, is_empty_now) {
851 (false, true) => self.empty_diagnostics_count += 1,
852 (true, false) => self.empty_diagnostics_count -= 1,
853 _ => {}
854 }
855 self.diagnostics.insert(key, info);
856 }
857
858 pub fn store_log(&mut self, level: LogLevel, message: String) {
863 let entry = LogEntry {
864 level,
865 message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
866 timestamp: Utc::now(),
867 };
868
869 if self.logs.len() >= MAX_LOG_ENTRIES {
870 self.logs.pop_front();
871 }
872 self.logs.push_back(entry);
873 }
874
875 pub fn store_message(&mut self, message_type: MessageType, message: String) {
880 let msg = ServerMessage {
881 message_type,
882 message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
883 timestamp: Utc::now(),
884 };
885
886 if self.messages.len() >= MAX_SERVER_MESSAGES {
887 self.messages.pop_front();
888 }
889 self.messages.push_back(msg);
890 }
891
892 #[inline]
900 #[must_use]
901 pub fn diagnostics(&self, uri: &str) -> Option<&DiagnosticInfo> {
902 self.diagnostics.get(uri_cache_key(uri).as_ref())
903 }
904
905 #[inline]
910 #[must_use]
911 pub fn diagnostics_owner(&self, uri: &str) -> Option<&ServerId> {
912 self.diagnostics_owners.get(uri_cache_key(uri).as_ref())
913 }
914
915 #[inline]
917 #[must_use]
918 pub const fn logs(&self) -> &VecDeque<LogEntry> {
919 &self.logs
920 }
921
922 #[inline]
924 #[must_use]
925 pub const fn messages(&self) -> &VecDeque<ServerMessage> {
926 &self.messages
927 }
928
929 pub fn clear_diagnostics(&mut self, uri: &str) -> Option<DiagnosticInfo> {
933 let key = uri_cache_key(uri).into_owned();
934 if let Some(owner) = self.diagnostics_owners.remove(&key)
935 && let Some(seq) = self.diagnostic_seq.remove(&key)
936 && let Some(order) = self.diagnostic_order.get_mut(&owner)
937 {
938 order.remove(&seq);
939 }
940 let removed = self.diagnostics.remove(&key);
941 if removed
942 .as_ref()
943 .is_some_and(|info| info.diagnostics.is_empty())
944 {
945 self.empty_diagnostics_count -= 1;
946 }
947 removed
948 }
949
950 pub fn clear_server_diagnostics(&mut self, server_id: &ServerId) {
977 let Some(order) = self.diagnostic_order.remove(server_id) else {
978 return;
979 };
980 for (_, key) in order {
981 if self
982 .diagnostics
983 .remove(&key)
984 .is_some_and(|info| info.diagnostics.is_empty())
985 {
986 self.empty_diagnostics_count -= 1;
987 }
988 self.diagnostics_owners.remove(&key);
989 self.diagnostic_seq.remove(&key);
990 }
991 }
992
993 pub fn mark_push_degraded(&mut self, server_id: &ServerId) {
1010 self.push_degraded.insert(server_id.clone());
1011 }
1012
1013 #[inline]
1019 #[must_use]
1020 pub fn is_push_degraded(&self, server_id: &ServerId) -> bool {
1021 self.push_degraded.contains(server_id)
1022 }
1023
1024 pub fn clear_all_diagnostics(&mut self) {
1026 self.diagnostics.clear();
1027 self.diagnostics_owners.clear();
1028 self.diagnostic_order.clear();
1029 self.diagnostic_seq.clear();
1030 self.empty_diagnostics_count = 0;
1031 }
1032
1033 pub fn clear_logs(&mut self) {
1035 self.logs.clear();
1036 }
1037
1038 pub fn clear_messages(&mut self) {
1040 self.messages.clear();
1041 }
1042
1043 #[inline]
1045 #[must_use]
1046 pub fn diagnostics_count(&self) -> usize {
1047 self.diagnostics.len()
1048 }
1049
1050 #[inline]
1052 #[must_use]
1053 pub fn logs_count(&self) -> usize {
1054 self.logs.len()
1055 }
1056
1057 #[inline]
1059 #[must_use]
1060 pub fn messages_count(&self) -> usize {
1061 self.messages.len()
1062 }
1063}
1064
1065#[cfg(test)]
1066#[allow(clippy::unwrap_used)]
1067mod tests {
1068 use lsp_types::{Position, Range};
1069
1070 use super::*;
1071 use crate::test_lsp::CapturedLogs;
1072
1073 fn test_server() -> ServerId {
1077 ServerId::from("test-server")
1078 }
1079
1080 #[test]
1081 fn test_notification_cache_new() {
1082 let cache = NotificationCache::new();
1083 assert_eq!(cache.diagnostics_count(), 0);
1084 assert_eq!(cache.logs_count(), 0);
1085 assert_eq!(cache.messages_count(), 0);
1086 }
1087
1088 #[test]
1089 fn test_store_and_diagnostics() {
1090 let mut cache = NotificationCache::new();
1091 let uri: Uri = Uri::from("file:///test.rs");
1092
1093 let diagnostic = LspDiagnostic {
1094 range: Range {
1095 start: Position {
1096 line: 0,
1097 character: 0,
1098 },
1099 end: Position {
1100 line: 0,
1101 character: 5,
1102 },
1103 },
1104 severity: Some(lsp_types::DiagnosticSeverity::Error),
1105 message: "test error".to_string().into(),
1106 code: None,
1107 source: None,
1108 code_description: None,
1109 related_information: None,
1110 tags: None,
1111 data: None,
1112 };
1113
1114 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1115
1116 let stored = cache.diagnostics(uri.as_ref()).unwrap();
1117 assert_eq!(stored.uri, uri);
1118 assert_eq!(stored.version, Some(1));
1119 assert_eq!(stored.diagnostics.len(), 1);
1120 assert_eq!(
1121 stored.diagnostics[0].message,
1122 lsp_types::Message::String("test error".to_string())
1123 );
1124 }
1125
1126 #[test]
1129 fn test_store_diagnostics_truncates_oversized_message() {
1130 let mut cache = NotificationCache::new();
1131 let uri: Uri = Uri::from("file:///test.rs");
1132 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
1133
1134 let diagnostic = LspDiagnostic {
1135 range: Range {
1136 start: Position {
1137 line: 0,
1138 character: 0,
1139 },
1140 end: Position {
1141 line: 0,
1142 character: 5,
1143 },
1144 },
1145 severity: Some(lsp_types::DiagnosticSeverity::Error),
1146 message: oversized.clone().into(),
1147 code: None,
1148 source: None,
1149 code_description: None,
1150 related_information: None,
1151 tags: None,
1152 data: None,
1153 };
1154
1155 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1156
1157 let stored = cache.diagnostics(uri.as_ref()).unwrap();
1158 let stored = message_as_str(&stored.diagnostics[0].message);
1159 assert!(stored.len() < oversized.len());
1160 assert!(stored.ends_with("... (truncated)"));
1161 }
1162
1163 fn minimal_diagnostic(message: String) -> LspDiagnostic {
1166 LspDiagnostic {
1167 range: Range {
1168 start: Position {
1169 line: 0,
1170 character: 0,
1171 },
1172 end: Position {
1173 line: 0,
1174 character: 5,
1175 },
1176 },
1177 severity: Some(lsp_types::DiagnosticSeverity::Error),
1178 message: message.into(),
1179 code: None,
1180 source: None,
1181 code_description: None,
1182 related_information: None,
1183 tags: None,
1184 data: None,
1185 }
1186 }
1187
1188 #[test]
1192 fn test_store_diagnostics_caps_aggregate_size_for_many_small_diagnostics() {
1193 let mut cache = NotificationCache::new();
1194 let uri: Uri = Uri::from("file:///test.rs");
1195
1196 let diagnostics: Vec<LspDiagnostic> = (0..5000)
1200 .map(|i| {
1201 minimal_diagnostic(format!(
1202 "diagnostic number {i}, padded: {}",
1203 "x".repeat(200)
1204 ))
1205 })
1206 .collect();
1207 let original_count = diagnostics.len();
1208
1209 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1210
1211 let stored = &cache.diagnostics(uri.as_ref()).unwrap().diagnostics;
1212 assert!(
1213 stored.len() < original_count,
1214 "aggregate cap must trim the list, kept {} of {original_count}",
1215 stored.len()
1216 );
1217 assert!(!stored.is_empty(), "must keep at least one diagnostic");
1218 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1219 assert!(
1220 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1221 "stored entry must fit the aggregate cap, got {serialized_len} bytes"
1222 );
1223 }
1224
1225 #[test]
1229 fn test_store_diagnostics_truncation_keeps_largest_fitting_prefix() {
1230 let mut cache = NotificationCache::new();
1231 let uri: Uri = Uri::from("file:///test.rs");
1232
1233 let diagnostics: Vec<LspDiagnostic> = (0..5000)
1237 .map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
1238 .collect();
1239
1240 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1241
1242 let stored = &cache.diagnostics(uri.as_ref()).unwrap().diagnostics;
1243 assert!(
1244 stored.len() > 2600,
1245 "largest-fitting-prefix search must keep far more than half, kept {}",
1246 stored.len()
1247 );
1248 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1249 assert!(serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES);
1250 let mut with_one_more = stored.clone();
1254 with_one_more.push(minimal_diagnostic(format!(
1255 "diagnostic overflow: {}",
1256 "x".repeat(250)
1257 )));
1258 assert!(
1259 serde_json::to_vec(&with_one_more).unwrap().len() > MAX_DIAGNOSTICS_ENTRY_BYTES,
1260 "kept count must be the largest that fits, not merely a fitting count"
1261 );
1262 }
1263
1264 #[test]
1268 fn test_store_diagnostics_truncation_prefers_higher_severity() {
1269 let mut cache = NotificationCache::new();
1270 let uri: Uri = Uri::from("file:///test.rs");
1271
1272 let mut diagnostics: Vec<LspDiagnostic> = (0..5000)
1273 .map(|i| {
1274 let mut d = minimal_diagnostic(format!("hint {i}: {}", "x".repeat(200)));
1275 d.severity = Some(lsp_types::DiagnosticSeverity::Hint);
1276 d
1277 })
1278 .collect();
1279 let mut trailing_error = minimal_diagnostic("the one real error".to_string());
1280 trailing_error.severity = Some(lsp_types::DiagnosticSeverity::Error);
1281 diagnostics.push(trailing_error);
1282
1283 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1284
1285 let stored = &cache.diagnostics(uri.as_ref()).unwrap().diagnostics;
1286 assert!(
1287 stored
1288 .iter()
1289 .any(|d| message_as_str(&d.message) == "the one real error"),
1290 "the trailing ERROR diagnostic must survive truncation over leading HINT noise"
1291 );
1292 }
1293
1294 #[test]
1298 fn test_store_diagnostics_warns_when_truncating_list() {
1299 use tracing_subscriber::layer::SubscriberExt as _;
1300
1301 let mut cache = NotificationCache::new();
1302 let uri: Uri = Uri::from("file:///test.rs");
1303 let diagnostics: Vec<LspDiagnostic> = (0..5000)
1304 .map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
1305 .collect();
1306
1307 let captured = CapturedLogs::default();
1308 let subscriber = tracing_subscriber::registry().with(captured.clone());
1309 let guard = tracing::subscriber::set_default(subscriber);
1310 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1311 drop(guard);
1312
1313 let messages = captured.messages();
1314 assert!(
1315 messages
1316 .iter()
1317 .any(|m| m.contains("highest-severity") && m.contains("file:///test.rs")),
1318 "expected a truncation warning naming the URI, got: {messages:?}"
1319 );
1320 }
1321
1322 #[test]
1326 fn test_store_diagnostics_warns_when_dropping_data_blob() {
1327 use tracing_subscriber::layer::SubscriberExt as _;
1328
1329 let mut cache = NotificationCache::new();
1330 let uri: Uri = Uri::from("file:///test.rs");
1331 let mut diagnostic = minimal_diagnostic("small message".to_string());
1332 diagnostic.data = Some(serde_json::json!({
1333 "blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
1334 }));
1335
1336 let captured = CapturedLogs::default();
1337 let subscriber = tracing_subscriber::registry().with(captured.clone());
1338 let guard = tracing::subscriber::set_default(subscriber);
1339 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1340 drop(guard);
1341
1342 let messages = captured.messages();
1343 assert!(
1344 messages.iter().any(|m| m.contains("code-action")),
1345 "expected a warning noting the code-action quick-fix impact, got: {messages:?}"
1346 );
1347 }
1348
1349 #[test]
1354 fn test_store_diagnostics_drops_oversized_data_blob_on_single_diagnostic() {
1355 let mut cache = NotificationCache::new();
1356 let uri: Uri = Uri::from("file:///test.rs");
1357
1358 let mut diagnostic = minimal_diagnostic("small message".to_string());
1359 diagnostic.data = Some(serde_json::json!({
1360 "blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
1361 }));
1362
1363 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1364
1365 let stored = &cache.diagnostics(uri.as_ref()).unwrap().diagnostics;
1366 assert_eq!(stored.len(), 1);
1367 assert_eq!(
1368 stored[0].message,
1369 lsp_types::Message::String("small message".to_string())
1370 );
1371 assert!(
1372 stored[0].data.is_none(),
1373 "oversized data blob must be dropped"
1374 );
1375 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1376 assert!(
1377 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1378 "stored entry must fit the aggregate cap after dropping data, got {serialized_len} bytes"
1379 );
1380 }
1381
1382 #[test]
1387 fn test_store_diagnostics_truncates_oversized_source_on_single_diagnostic() {
1388 let mut cache = NotificationCache::new();
1389 let uri: Uri = Uri::from("file:///test.rs");
1390
1391 let mut diagnostic = minimal_diagnostic("small message".to_string());
1392 diagnostic.source = Some("x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000));
1393
1394 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1395
1396 let stored = &cache.diagnostics(uri.as_ref()).unwrap().diagnostics;
1397 assert_eq!(stored.len(), 1);
1398 assert_eq!(
1399 stored[0].message,
1400 lsp_types::Message::String("small message".to_string())
1401 );
1402 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1403 assert!(
1404 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1405 "stored entry must fit the aggregate cap after truncating source, got {serialized_len} bytes"
1406 );
1407 }
1408
1409 #[test]
1416 fn test_store_diagnostics_caps_single_diagnostic_with_every_field_maxed_out() {
1417 let mut cache = NotificationCache::new();
1418 let uri: Uri = Uri::from("file:///test.rs");
1419
1420 let mut diagnostic = minimal_diagnostic("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
1425 diagnostic.source = Some("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
1426 diagnostic.code = Some(lsp_types::Code::String(
1427 "x".repeat(MAX_ENTRY_TEXT_BYTES + 1000),
1428 ));
1429 diagnostic.data = Some(serde_json::json!({ "blob": "x".repeat(MAX_ENTRY_TEXT_BYTES) }));
1430 diagnostic.tags = Some(vec![lsp_types::DiagnosticTag::Unnecessary; 50]);
1431 diagnostic.related_information = Some(vec![
1432 lsp_types::DiagnosticRelatedInformation {
1433 location: lsp_types::Location {
1434 uri: uri.clone(),
1435 range: Range::default(),
1436 },
1437 message: "x".repeat(1000),
1438 };
1439 5
1440 ]);
1441
1442 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1443
1444 let stored = &cache.diagnostics(uri.as_ref()).unwrap().diagnostics;
1445 assert_eq!(stored.len(), 1);
1446 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1447 assert!(
1448 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1449 "postcondition must hold even with every field maxed out, got {serialized_len} bytes"
1450 );
1451 }
1452
1453 #[test]
1462 fn test_cap_diagnostics_entry_size_terminal_fallback_bounds_untruncated_message() {
1463 let uri: Uri = Uri::from("file:///test.rs");
1464 let mut diagnostics = vec![minimal_diagnostic(
1465 "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
1466 )];
1467
1468 cap_diagnostics_entry_size(&uri, &mut diagnostics);
1469
1470 assert_eq!(diagnostics.len(), 1);
1471 assert!(
1472 message_as_str(&diagnostics[0].message).len()
1473 <= DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES + 20,
1474 "terminal fallback must truncate the message itself, got {} bytes",
1475 message_as_str(&diagnostics[0].message).len()
1476 );
1477 let serialized_len = serde_json::to_vec(&diagnostics).unwrap().len();
1478 assert!(
1479 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1480 "postcondition must hold via the terminal fallback, got {serialized_len} bytes"
1481 );
1482 }
1483
1484 #[test]
1489 fn test_store_diagnostics_cheap_path_leaves_small_diagnostics_untouched() {
1490 let mut cache = NotificationCache::new();
1491 let uri: Uri = Uri::from("file:///test.rs");
1492
1493 let mut diagnostic = minimal_diagnostic("a small, ordinary diagnostic message".to_string());
1494 diagnostic.source = Some("rustc".to_string());
1495
1496 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1497
1498 let stored = &cache.diagnostics(uri.as_ref()).unwrap().diagnostics;
1499 assert_eq!(stored.len(), 1);
1500 assert_eq!(
1501 stored[0].message,
1502 lsp_types::Message::String("a small, ordinary diagnostic message".to_string())
1503 );
1504 assert_eq!(stored[0].source.as_deref(), Some("rustc"));
1505 }
1506
1507 #[test]
1517 fn test_store_diagnostics_cheap_path_escape_safe_for_control_character_heavy_message() {
1518 let mut cache = NotificationCache::new();
1519 let uri: Uri = Uri::from("file:///test.rs");
1520
1521 let nul_heavy_message = "\0".repeat(MAX_ENTRY_TEXT_BYTES);
1522 let diagnostics: Vec<LspDiagnostic> = (0..3)
1523 .map(|_| minimal_diagnostic(nul_heavy_message.clone()))
1524 .collect();
1525
1526 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1527
1528 let stored = &cache.diagnostics(uri.as_ref()).unwrap().diagnostics;
1529 let serialized_len = serde_json::to_vec(stored).unwrap().len();
1530 assert!(
1531 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
1532 "escape-heavy content must not let the cheap-estimate fast path skip the real cap, \
1533 got {serialized_len} bytes"
1534 );
1535 }
1536
1537 #[test]
1538 fn test_store_diagnostics_replaces_existing() {
1539 let mut cache = NotificationCache::new();
1540 let uri: Uri = Uri::from("file:///test.rs");
1541
1542 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1543 assert_eq!(cache.diagnostics_count(), 1);
1544
1545 cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
1546 assert_eq!(cache.diagnostics_count(), 1);
1547
1548 let stored = cache.diagnostics(uri.as_ref()).unwrap();
1549 assert_eq!(stored.version, Some(2));
1550 }
1551
1552 #[test]
1553 fn test_clear_diagnostics() {
1554 let mut cache = NotificationCache::new();
1555 let uri: Uri = Uri::from("file:///test.rs");
1556
1557 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1558 assert_eq!(cache.diagnostics_count(), 1);
1559
1560 let cleared = cache.clear_diagnostics(uri.as_ref());
1561 assert!(cleared.is_some());
1562 assert_eq!(cache.diagnostics_count(), 0);
1563 }
1564
1565 #[test]
1566 fn test_clear_all_diagnostics() {
1567 let mut cache = NotificationCache::new();
1568 let uri1: Uri = Uri::from("file:///test1.rs");
1569 let uri2: Uri = Uri::from("file:///test2.rs");
1570
1571 cache.store_diagnostics(&test_server(), &uri1, Some(1), vec![]);
1572 cache.store_diagnostics(&test_server(), &uri2, Some(1), vec![]);
1573 assert_eq!(cache.diagnostics_count(), 2);
1574
1575 cache.clear_all_diagnostics();
1576 assert_eq!(cache.diagnostics_count(), 0);
1577 }
1578
1579 #[test]
1580 fn test_store_and_get_logs() {
1581 let mut cache = NotificationCache::new();
1582
1583 cache.store_log(LogLevel::Error, "error message".to_string());
1584 cache.store_log(LogLevel::Info, "info message".to_string());
1585
1586 let logs = cache.logs();
1587 assert_eq!(logs.len(), 2);
1588 assert_eq!(logs[0].level, LogLevel::Error);
1589 assert_eq!(logs[0].message, "error message");
1590 assert_eq!(logs[1].level, LogLevel::Info);
1591 assert_eq!(logs[1].message, "info message");
1592 }
1593
1594 #[test]
1595 fn test_logs_max_capacity() {
1596 let mut cache = NotificationCache::new();
1597
1598 for i in 0..MAX_LOG_ENTRIES + 10 {
1600 cache.store_log(LogLevel::Info, format!("message {i}"));
1601 }
1602
1603 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
1604
1605 let logs = cache.logs();
1607 assert_eq!(logs.front().unwrap().message, "message 10");
1608 assert_eq!(
1609 logs.back().unwrap().message,
1610 format!("message {}", MAX_LOG_ENTRIES + 9)
1611 );
1612 }
1613
1614 #[test]
1618 fn test_store_log_truncates_oversized_message() {
1619 let mut cache = NotificationCache::new();
1620 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
1621
1622 cache.store_log(LogLevel::Info, oversized.clone());
1623
1624 let stored = &cache.logs()[0].message;
1625 assert!(stored.len() < oversized.len());
1626 assert!(stored.ends_with("... (truncated)"));
1627 }
1628
1629 #[test]
1630 fn test_store_log_does_not_truncate_message_at_or_below_limit() {
1631 let mut cache = NotificationCache::new();
1632 let message = "a".repeat(MAX_ENTRY_TEXT_BYTES);
1633
1634 cache.store_log(LogLevel::Info, message.clone());
1635
1636 assert_eq!(cache.logs()[0].message, message);
1637 }
1638
1639 #[test]
1640 fn test_clear_logs() {
1641 let mut cache = NotificationCache::new();
1642 cache.store_log(LogLevel::Info, "test".to_string());
1643 assert_eq!(cache.logs_count(), 1);
1644
1645 cache.clear_logs();
1646 assert_eq!(cache.logs_count(), 0);
1647 }
1648
1649 #[test]
1650 fn test_store_and_get_messages() {
1651 let mut cache = NotificationCache::new();
1652
1653 cache.store_message(MessageType::Error, "error msg".to_string());
1654 cache.store_message(MessageType::Warning, "warning msg".to_string());
1655
1656 let messages = cache.messages();
1657 assert_eq!(messages.len(), 2);
1658 assert_eq!(messages[0].message_type, MessageType::Error);
1659 assert_eq!(messages[0].message, "error msg");
1660 assert_eq!(messages[1].message_type, MessageType::Warning);
1661 assert_eq!(messages[1].message, "warning msg");
1662 }
1663
1664 #[test]
1665 fn test_messages_max_capacity() {
1666 let mut cache = NotificationCache::new();
1667
1668 for i in 0..MAX_SERVER_MESSAGES + 10 {
1670 cache.store_message(MessageType::Info, format!("message {i}"));
1671 }
1672
1673 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
1674
1675 let messages = cache.messages();
1677 assert_eq!(messages.front().unwrap().message, "message 10");
1678 assert_eq!(
1679 messages.back().unwrap().message,
1680 format!("message {}", MAX_SERVER_MESSAGES + 9)
1681 );
1682 }
1683
1684 #[test]
1685 fn test_clear_messages() {
1686 let mut cache = NotificationCache::new();
1687 cache.store_message(MessageType::Info, "test".to_string());
1688 assert_eq!(cache.messages_count(), 1);
1689
1690 cache.clear_messages();
1691 assert_eq!(cache.messages_count(), 0);
1692 }
1693
1694 #[test]
1696 fn test_store_message_truncates_oversized_message() {
1697 let mut cache = NotificationCache::new();
1698 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
1699
1700 cache.store_message(MessageType::Info, oversized.clone());
1701
1702 let stored = &cache.messages()[0].message;
1703 assert!(stored.len() < oversized.len());
1704 assert!(stored.ends_with("... (truncated)"));
1705 }
1706
1707 #[test]
1708 fn test_log_levels() {
1709 let mut cache = NotificationCache::new();
1710
1711 cache.store_log(LogLevel::Error, "error".to_string());
1712 cache.store_log(LogLevel::Warning, "warning".to_string());
1713 cache.store_log(LogLevel::Info, "info".to_string());
1714 cache.store_log(LogLevel::Debug, "debug".to_string());
1715
1716 let logs = cache.logs();
1717 assert_eq!(logs[0].level, LogLevel::Error);
1718 assert_eq!(logs[1].level, LogLevel::Warning);
1719 assert_eq!(logs[2].level, LogLevel::Info);
1720 assert_eq!(logs[3].level, LogLevel::Debug);
1721 }
1722
1723 #[test]
1724 fn test_message_types() {
1725 let mut cache = NotificationCache::new();
1726
1727 cache.store_message(MessageType::Error, "error".to_string());
1728 cache.store_message(MessageType::Warning, "warning".to_string());
1729 cache.store_message(MessageType::Info, "info".to_string());
1730 cache.store_message(MessageType::Log, "log".to_string());
1731
1732 let messages = cache.messages();
1733 assert_eq!(messages[0].message_type, MessageType::Error);
1734 assert_eq!(messages[1].message_type, MessageType::Warning);
1735 assert_eq!(messages[2].message_type, MessageType::Info);
1736 assert_eq!(messages[3].message_type, MessageType::Log);
1737 }
1738
1739 #[test]
1740 fn test_timestamp_ordering() {
1741 let mut cache = NotificationCache::new();
1742
1743 cache.store_log(LogLevel::Info, "first".to_string());
1744 std::thread::sleep(std::time::Duration::from_millis(10));
1745 cache.store_log(LogLevel::Info, "second".to_string());
1746
1747 let logs = cache.logs();
1748 assert!(logs[0].timestamp < logs[1].timestamp);
1749 }
1750
1751 #[test]
1752 fn test_store_diagnostics_empty_list() {
1753 let mut cache = NotificationCache::new();
1754 let uri: Uri = Uri::from("file:///test.rs");
1755
1756 let diagnostic = LspDiagnostic {
1757 range: Range {
1758 start: Position {
1759 line: 0,
1760 character: 0,
1761 },
1762 end: Position {
1763 line: 0,
1764 character: 5,
1765 },
1766 },
1767 severity: Some(lsp_types::DiagnosticSeverity::Error),
1768 message: "test error".to_string().into(),
1769 code: None,
1770 source: None,
1771 code_description: None,
1772 related_information: None,
1773 tags: None,
1774 data: None,
1775 };
1776
1777 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
1778 assert_eq!(
1779 cache.diagnostics(uri.as_ref()).unwrap().diagnostics.len(),
1780 1
1781 );
1782
1783 cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
1784 let stored = cache.diagnostics(uri.as_ref()).unwrap();
1785 assert_eq!(stored.diagnostics.len(), 0);
1786 assert_eq!(stored.version, Some(2));
1787 }
1788
1789 #[test]
1790 fn test_store_many_diagnostics_single_file() {
1791 let mut cache = NotificationCache::new();
1792 let uri: Uri = Uri::from("file:///test.rs");
1793
1794 let diagnostics: Vec<LspDiagnostic> = (0..100)
1795 .map(|i| LspDiagnostic {
1796 range: Range {
1797 start: Position {
1798 line: i,
1799 character: 0,
1800 },
1801 end: Position {
1802 line: i,
1803 character: 10,
1804 },
1805 },
1806 message: format!("Error {i}").into(),
1807 severity: Some(lsp_types::DiagnosticSeverity::Error),
1808 code: None,
1809 source: None,
1810 code_description: None,
1811 related_information: None,
1812 tags: None,
1813 data: None,
1814 })
1815 .collect();
1816
1817 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
1818
1819 let stored = cache.diagnostics(uri.as_ref()).unwrap();
1820 assert_eq!(stored.diagnostics.len(), 100);
1821 }
1822
1823 #[test]
1824 fn test_logs_exact_capacity_boundary() {
1825 let mut cache = NotificationCache::new();
1826
1827 for i in 0..MAX_LOG_ENTRIES {
1828 cache.store_log(LogLevel::Info, format!("message {i}"));
1829 }
1830 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
1831
1832 cache.store_log(LogLevel::Info, "overflow".to_string());
1833 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
1834 assert_eq!(cache.logs().front().unwrap().message, "message 1");
1835 }
1836
1837 #[test]
1838 fn test_messages_exact_capacity_boundary() {
1839 let mut cache = NotificationCache::new();
1840
1841 for i in 0..MAX_SERVER_MESSAGES {
1842 cache.store_message(MessageType::Info, format!("message {i}"));
1843 }
1844 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
1845
1846 cache.store_message(MessageType::Info, "overflow".to_string());
1847 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
1848 assert_eq!(cache.messages().front().unwrap().message, "message 1");
1849 }
1850
1851 #[test]
1852 fn test_diagnostics_max_capacity() {
1853 let mut cache = NotificationCache::new();
1854
1855 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 10 {
1856 let uri: Uri = Uri::from(format!("file:///test{i}.rs"));
1857 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1858 }
1859
1860 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1861
1862 let evicted: Uri = Uri::from("file:///test0.rs");
1864 assert!(cache.diagnostics(evicted.as_ref()).is_none());
1865 let newest: Uri = Uri::from(format!("file:///test{}.rs", MAX_DIAGNOSTIC_ENTRIES + 9));
1866 assert!(cache.diagnostics(newest.as_ref()).is_some());
1867 }
1868
1869 #[test]
1870 fn test_diagnostics_replacing_existing_uri_does_not_trigger_eviction() {
1871 let mut cache = NotificationCache::new();
1872 let uri: Uri = Uri::from("file:///stable.rs");
1873
1874 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
1875 cache.store_diagnostics(
1876 &test_server(),
1877 &uri,
1878 Some(i32::try_from(i).unwrap()),
1879 vec![],
1880 );
1881 }
1882 assert_eq!(cache.diagnostics_count(), 1);
1883 assert!(cache.diagnostics(uri.as_ref()).is_some());
1884 }
1885
1886 #[test]
1887 fn test_diagnostics_republish_refreshes_eviction_order() {
1888 let mut cache = NotificationCache::new();
1892 let actively_edited: Uri = Uri::from("file:///keep.rs");
1893 cache.store_diagnostics(&test_server(), &actively_edited, Some(1), vec![]);
1894
1895 for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
1897 let uri: Uri = Uri::from(format!("file:///untouched{i}.rs"));
1898 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1899 }
1900 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1901
1902 cache.store_diagnostics(&test_server(), &actively_edited, Some(2), vec![]);
1906
1907 let overflow: Uri = Uri::from("file:///overflow.rs");
1910 cache.store_diagnostics(&test_server(), &overflow, Some(1), vec![]);
1911
1912 assert!(
1913 cache.diagnostics(actively_edited.as_ref()).is_some(),
1914 "republished entry must survive eviction after being refreshed"
1915 );
1916 let oldest_untouched: Uri = Uri::from("file:///untouched0.rs");
1917 assert!(
1918 cache.diagnostics(oldest_untouched.as_ref()).is_none(),
1919 "the oldest never-republished entry must be evicted instead"
1920 );
1921 assert!(cache.diagnostics(overflow.as_ref()).is_some());
1922 }
1923
1924 #[test]
1925 fn test_clear_diagnostics_then_refill_does_not_evict_early() {
1926 let mut cache = NotificationCache::new();
1927 let first: Uri = Uri::from("file:///first.rs");
1928 cache.store_diagnostics(&test_server(), &first, Some(1), vec![]);
1929 cache.clear_diagnostics(first.as_ref());
1930 assert_eq!(cache.diagnostics_count(), 0);
1931
1932 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
1933 let uri: Uri = Uri::from(format!("file:///test{i}.rs"));
1934 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
1935 }
1936 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1937 let first_of_batch: Uri = Uri::from("file:///test0.rs");
1941 assert!(cache.diagnostics(first_of_batch.as_ref()).is_some());
1942 }
1943
1944 #[test]
1945 fn test_clear_diagnostics_nonexistent() {
1946 let mut cache = NotificationCache::new();
1947 let result = cache.clear_diagnostics("file:///nonexistent.rs");
1948 assert!(result.is_none());
1949 }
1950
1951 #[test]
1952 fn test_store_diagnostics_no_version() {
1953 let mut cache = NotificationCache::new();
1954 let uri: Uri = Uri::from("file:///test.rs");
1955
1956 cache.store_diagnostics(&test_server(), &uri, None, vec![]);
1957 let stored = cache.diagnostics(uri.as_ref()).unwrap();
1958 assert_eq!(stored.version, None);
1959 }
1960
1961 #[test]
1967 fn test_noisy_server_does_not_evict_quiet_server_entries() {
1968 let mut cache = NotificationCache::new();
1969 cache.set_diagnostics_route_count(2);
1970 let noisy = ServerId::from("noisy");
1971 let quiet = ServerId::from("quiet");
1972
1973 let quiet_uri: Uri = Uri::from("file:///quiet/only_file.rs");
1974 cache.store_diagnostics(&quiet, &quiet_uri, Some(1), vec![]);
1975
1976 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 50 {
1981 let uri: Uri = Uri::from(format!("file:///noisy/file{i}.rs"));
1982 cache.store_diagnostics(&noisy, &uri, Some(1), vec![]);
1983 }
1984
1985 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
1986 assert!(
1987 cache.diagnostics(quiet_uri.as_ref()).is_some(),
1988 "quiet server's only entry must survive the noisy server's overflow"
1989 );
1990
1991 let noisy_first: Uri = Uri::from("file:///noisy/file0.rs");
1992 assert!(
1993 cache.diagnostics(noisy_first.as_ref()).is_none(),
1994 "noisy server's own oldest entries must be evicted once the aggregate cache is full"
1995 );
1996 }
1997
1998 #[test]
2004 fn test_dominant_server_exceeds_equal_share_while_others_idle() {
2005 let mut cache = NotificationCache::new();
2006 cache.set_diagnostics_route_count(4);
2007 let dominant = ServerId::from("dominant");
2008
2009 let equal_share = MAX_DIAGNOSTIC_ENTRIES / 4;
2010 let more_than_share = equal_share + 100;
2011 for i in 0..more_than_share {
2012 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
2013 cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
2014 }
2015 assert_eq!(
2016 cache.diagnostics_count(),
2017 more_than_share,
2018 "a dominant server must be able to exceed its static equal share while the aggregate has room"
2019 );
2020
2021 for i in more_than_share..MAX_DIAGNOSTIC_ENTRIES {
2024 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
2025 cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
2026 }
2027 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2028 }
2029
2030 #[test]
2043 fn test_eviction_target_tie_break_is_deterministic() {
2044 let mut cache = NotificationCache::new();
2045 cache.set_diagnostics_route_count(1000); let a = ServerId::from("a");
2048 let b = ServerId::from("b");
2049 for i in 0..2 {
2050 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
2051 cache.store_diagnostics(&a, &uri, Some(1), vec![]);
2052 }
2053 for i in 0..2 {
2054 let uri: Uri = Uri::from(format!("file:///b/file{i}.rs"));
2055 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
2056 }
2057
2058 let writer = ServerId::from("writer");
2062 assert_eq!(cache.server_to_evict_from(&writer), Some(b));
2063 }
2064
2065 #[test]
2074 fn test_new_writer_still_evicts_when_every_existing_server_is_in_share() {
2075 let mut cache = NotificationCache::new();
2076 cache.set_diagnostics_route_count(2); let a = ServerId::from("a");
2079 let b = ServerId::from("b");
2080 for i in 0..500 {
2081 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
2082 cache.store_diagnostics(&a, &uri, Some(1), vec![]);
2083 }
2084 for i in 0..500 {
2085 let uri: Uri = Uri::from(format!("file:///b/file{i}.rs"));
2086 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
2087 }
2088 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2089
2090 let c = ServerId::from("c");
2093 let new_uri: Uri = Uri::from("file:///c/first.rs");
2094 cache.store_diagnostics(&c, &new_uri, Some(1), vec![]);
2095
2096 assert_eq!(
2097 cache.diagnostics_count(),
2098 MAX_DIAGNOSTIC_ENTRIES,
2099 "the aggregate cap must still be enforced even when every existing server is within share"
2100 );
2101 assert!(cache.diagnostics(new_uri.as_ref()).is_some());
2102
2103 let b_oldest: Uri = Uri::from("file:///b/file0.rs");
2107 assert!(
2108 cache.diagnostics(b_oldest.as_ref()).is_none(),
2109 "the largest in-share server (tie-broken to b) must lose its oldest entry"
2110 );
2111 assert!(
2112 cache.diagnostics("file:///a/file0.rs").is_some(),
2113 "the other in-share server must be untouched"
2114 );
2115 }
2116
2117 #[test]
2120 fn test_repeated_writes_same_owner_do_not_grow_order_map() {
2121 let mut cache = NotificationCache::new();
2122 let server = ServerId::from("server");
2123 let uri: Uri = Uri::from("file:///test.rs");
2124
2125 let max_version = i32::try_from(MAX_DIAGNOSTIC_ENTRIES).unwrap() + 10;
2126 for version in 0..max_version {
2127 cache.store_diagnostics(&server, &uri, Some(version), vec![]);
2128 }
2129
2130 assert_eq!(cache.diagnostics_count(), 1);
2131 let stored = cache.diagnostics(uri.as_ref()).unwrap();
2132 assert_eq!(stored.version, Some(max_version - 1));
2133 }
2134
2135 #[test]
2139 fn test_store_diagnostics_reassigns_ownership() {
2140 let mut cache = NotificationCache::new();
2141 let old_owner = ServerId::from("old");
2142 let new_owner = ServerId::from("new");
2143 let uri: Uri = Uri::from("file:///test.rs");
2144
2145 cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
2146 cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
2147
2148 assert_eq!(cache.diagnostics_count(), 1);
2149 let stored = cache.diagnostics(uri.as_ref()).unwrap();
2150 assert_eq!(stored.version, Some(2));
2151
2152 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 5 {
2156 let other: Uri = Uri::from(format!("file:///old/file{i}.rs"));
2157 cache.store_diagnostics(&old_owner, &other, Some(1), vec![]);
2158 }
2159 assert!(cache.diagnostics(uri.as_ref()).is_some());
2160 }
2161
2162 #[test]
2167 fn test_diagnostics_owner_returns_publisher_after_store() {
2168 let mut cache = NotificationCache::new();
2169 let server = ServerId::from("rust");
2170 let uri: Uri = Uri::from("file:///main.rs");
2171
2172 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
2173
2174 assert_eq!(cache.diagnostics_owner(uri.as_ref()), Some(&server));
2175 }
2176
2177 #[test]
2178 fn test_diagnostics_owner_none_for_untracked_uri() {
2179 let cache = NotificationCache::new();
2180 let uri: Uri = Uri::from("file:///never-seen.rs");
2181
2182 assert_eq!(cache.diagnostics_owner(uri.as_ref()), None);
2183 }
2184
2185 #[test]
2190 fn test_diagnostics_owner_reflects_reassigned_ownership() {
2191 let mut cache = NotificationCache::new();
2192 let old_owner = ServerId::from("old");
2193 let new_owner = ServerId::from("new");
2194 let uri: Uri = Uri::from("file:///test.rs");
2195
2196 cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
2197 assert_eq!(cache.diagnostics_owner(uri.as_ref()), Some(&old_owner));
2198
2199 cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
2200 assert_eq!(cache.diagnostics_owner(uri.as_ref()), Some(&new_owner));
2201 }
2202
2203 #[test]
2206 fn test_clear_server_diagnostics_scopes_to_one_server() {
2207 let mut cache = NotificationCache::new();
2208 let crashed = ServerId::from("crashed");
2209 let healthy = ServerId::from("healthy");
2210
2211 let crashed_uri: Uri = Uri::from("file:///crashed/main.py");
2212 let healthy_uri: Uri = Uri::from("file:///healthy/main.rs");
2213 cache.store_diagnostics(&crashed, &crashed_uri, Some(1), vec![]);
2214 cache.store_diagnostics(&healthy, &healthy_uri, Some(1), vec![]);
2215
2216 cache.clear_server_diagnostics(&crashed);
2217
2218 assert!(cache.diagnostics(crashed_uri.as_ref()).is_none());
2219 assert!(cache.diagnostics(healthy_uri.as_ref()).is_some());
2220 assert_eq!(cache.diagnostics_count(), 1);
2221
2222 cache.clear_server_diagnostics(&crashed);
2224 assert_eq!(cache.diagnostics_count(), 1);
2225 }
2226
2227 #[test]
2232 fn test_push_degraded_is_scoped_per_server_and_permanent() {
2233 let mut cache = NotificationCache::new();
2234 let degraded = ServerId::from("degraded");
2235 let healthy = ServerId::from("healthy");
2236
2237 assert!(!cache.is_push_degraded(°raded));
2238 assert!(!cache.is_push_degraded(&healthy));
2239
2240 cache.mark_push_degraded(°raded);
2241
2242 assert!(cache.is_push_degraded(°raded));
2243 assert!(!cache.is_push_degraded(&healthy));
2244
2245 cache.mark_push_degraded(°raded);
2247 assert!(cache.is_push_degraded(°raded));
2248 }
2249
2250 #[test]
2258 fn test_shrinking_budget_affects_eviction_target_not_existing_entries() {
2259 let mut cache = NotificationCache::new();
2260 let server = ServerId::from("server");
2261
2262 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
2263 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
2264 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
2265 }
2266 assert_eq!(
2267 cache.diagnostics_count(),
2268 MAX_DIAGNOSTIC_ENTRIES,
2269 "filling to the aggregate cap must not evict anything early"
2270 );
2271
2272 cache.set_diagnostics_route_count(4);
2275 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2276
2277 let other = ServerId::from("other");
2280 let new_uri: Uri = Uri::from("file:///other/new.rs");
2281 cache.store_diagnostics(&other, &new_uri, Some(1), vec![]);
2282
2283 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2284 assert!(cache.diagnostics(new_uri.as_ref()).is_some());
2285 let server_oldest: Uri = Uri::from("file:///file0.rs");
2286 assert!(
2287 cache.diagnostics(server_oldest.as_ref()).is_none(),
2288 "the pre-existing server's oldest entry, now far over its shrunk share, must be evicted"
2289 );
2290 }
2291
2292 #[test]
2298 fn test_fair_share_applies_by_default_without_explicit_route_count() {
2299 let mut cache = NotificationCache::new();
2300 let noisy = ServerId::from("noisy");
2301 let quiet = ServerId::from("quiet");
2302
2303 let quiet_uri: Uri = Uri::from("file:///quiet/only_file.rs");
2304 cache.store_diagnostics(&quiet, &quiet_uri, Some(1), vec![]);
2305
2306 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 50 {
2308 let uri: Uri = Uri::from(format!("file:///noisy/file{i}.rs"));
2309 cache.store_diagnostics(&noisy, &uri, Some(1), vec![]);
2310 }
2311
2312 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2313 assert!(
2314 cache.diagnostics(quiet_uri.as_ref()).is_some(),
2315 "quiet server's only entry must survive even without ever calling \
2316 set_diagnostics_route_count"
2317 );
2318 let noisy_first: Uri = Uri::from("file:///noisy/file0.rs");
2319 assert!(
2320 cache.diagnostics(noisy_first.as_ref()).is_none(),
2321 "the noisy server, now auto-derived as one of two servers sharing the budget, \
2322 must still lose its own oldest entries once over its fair share"
2323 );
2324 }
2325
2326 #[test]
2332 fn test_single_server_gets_full_budget_without_explicit_route_count() {
2333 let mut cache = NotificationCache::new();
2334
2335 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
2336 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
2337 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
2338 }
2339
2340 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2341 }
2342
2343 #[test]
2349 fn test_empty_diagnostics_entries_evicted_before_non_empty_ones() {
2350 let mut cache = NotificationCache::new();
2351 let server = test_server();
2352
2353 let important: Uri = Uri::from("file:///important.rs");
2354 cache.store_diagnostics(
2355 &server,
2356 &important,
2357 Some(1),
2358 vec![minimal_diagnostic("real error".to_string())],
2359 );
2360
2361 for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
2365 let uri: Uri = Uri::from(format!("file:///clean{i}.rs"));
2366 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
2367 }
2368 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2369
2370 let overflow: Uri = Uri::from("file:///overflow.rs");
2374 cache.store_diagnostics(&server, &overflow, Some(1), vec![]);
2375
2376 assert!(
2377 cache.diagnostics(important.as_ref()).is_some(),
2378 "a non-empty entry must survive eviction over empty entries, even though it is older"
2379 );
2380 let oldest_clean: Uri = Uri::from("file:///clean0.rs");
2381 assert!(
2382 cache.diagnostics(oldest_clean.as_ref()).is_none(),
2383 "the oldest empty entry must be evicted instead of the older non-empty one"
2384 );
2385 assert!(cache.diagnostics(overflow.as_ref()).is_some());
2386 }
2387
2388 #[test]
2396 fn test_empty_diagnostics_entry_is_still_tracked_until_evicted() {
2397 let mut cache = NotificationCache::new();
2398 let uri: Uri = Uri::from("file:///clean.rs");
2399
2400 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
2401
2402 let stored = cache.diagnostics(uri.as_ref());
2403 assert!(
2404 stored.is_some(),
2405 "an empty-diagnostics entry must still be tracked"
2406 );
2407 assert_eq!(stored.unwrap().diagnostics.len(), 0);
2408 }
2409
2410 #[test]
2415 fn test_over_share_servers_empty_entry_evicted_before_a_different_servers_real_diagnostic() {
2416 let mut cache = NotificationCache::new();
2417 cache.set_diagnostics_route_count(3); let a = ServerId::from("a"); let b = ServerId::from("b"); let c = ServerId::from("c"); for i in 0..500 {
2424 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
2425 cache.store_diagnostics(
2426 &a,
2427 &uri,
2428 Some(1),
2429 vec![minimal_diagnostic(format!("error {i}"))],
2430 );
2431 }
2432 for i in 0..400 {
2433 let uri: Uri = Uri::from(format!("file:///b/file{i}.rs"));
2434 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
2435 }
2436 for i in 0..100 {
2437 let uri: Uri = Uri::from(format!("file:///c/file{i}.rs"));
2438 cache.store_diagnostics(&c, &uri, Some(1), vec![]);
2439 }
2440 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2441
2442 let overflow: Uri = Uri::from("file:///a/overflow.rs");
2447 cache.store_diagnostics(
2448 &a,
2449 &overflow,
2450 Some(1),
2451 vec![minimal_diagnostic("overflow error".to_string())],
2452 );
2453
2454 for i in 0..500 {
2455 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
2456 assert!(
2457 cache.diagnostics(uri.as_ref()).is_some(),
2458 "server a's real diagnostics must all survive; b has an empty entry to lose \
2459 instead"
2460 );
2461 }
2462 let b_oldest: Uri = Uri::from("file:///b/file0.rs");
2463 assert!(
2464 cache.diagnostics(b_oldest.as_ref()).is_none(),
2465 "b's oldest empty entry must be evicted instead of a's real diagnostics"
2466 );
2467 assert!(cache.diagnostics(overflow.as_ref()).is_some());
2468 }
2469
2470 #[test]
2475 fn test_dirty_then_clean_then_dirty_again_updates_emptiness_tracking() {
2476 let mut cache = NotificationCache::new();
2477 let server = test_server();
2478 let uri: Uri = Uri::from("file:///flapping.rs");
2479
2480 cache.store_diagnostics(
2481 &server,
2482 &uri,
2483 Some(1),
2484 vec![minimal_diagnostic("first error".to_string())],
2485 );
2486 cache.store_diagnostics(&server, &uri, Some(2), vec![]); cache.store_diagnostics(
2488 &server,
2489 &uri,
2490 Some(3),
2491 vec![minimal_diagnostic("second error".to_string())],
2492 ); for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
2498 let other: Uri = Uri::from(format!("file:///clean{i}.rs"));
2499 cache.store_diagnostics(&server, &other, Some(1), vec![]);
2500 }
2501 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2502
2503 let overflow: Uri = Uri::from("file:///overflow.rs");
2504 cache.store_diagnostics(&server, &overflow, Some(1), vec![]);
2505
2506 let stored = cache.diagnostics(uri.as_ref());
2507 assert!(
2508 stored.is_some_and(|info| info.diagnostics.len() == 1),
2509 "the re-dirtied entry must survive and keep its real diagnostic, not be mistaken \
2510 for an empty entry"
2511 );
2512 }
2513
2514 #[test]
2517 fn test_set_diagnostics_route_count_zero_clamps_to_one() {
2518 let mut cache = NotificationCache::new();
2519 cache.set_diagnostics_route_count(0);
2520
2521 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 5 {
2522 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
2523 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
2524 }
2525
2526 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
2527 }
2528}