1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
6use std::sync::Arc;
7
8use chrono::{DateTime, Utc};
9use lsp_types::{Diagnostic as LspDiagnostic, Uri};
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use tracing::{debug, warn};
13
14use crate::bridge::indexing::{IndexingPolicy, IndexingReset, IndexingState, IndexingTracker};
15use crate::bridge::resources::{CanonicalForm, DiagnosticsResourceUri, PublishedDiagnosticsUri};
16use crate::bridge::{
17 DiagnosticsAvailability, DocumentTracker, DocumentVersion, Opening, uri_to_path,
18};
19use crate::config::{BoundedText, ServerId};
20use crate::util::{WarnLimiter, truncate_string};
21
22mod bounds;
23mod pulled_index;
24
25pub use bounds::BoundedDiagnostics;
26use bounds::{MAX_ENTRY_TEXT_BYTES, cap_diagnostics_entry_size};
27use pulled_index::{PulledIndex, point};
28pub use pulled_index::{ReportedSeverity, reported_code};
29
30const MAX_LOG_ENTRIES: usize = 100;
32
33pub const MAX_DIAGNOSTIC_ENTRIES: usize = 1000;
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
69pub struct DiagnosticsKey(String);
70
71impl DiagnosticsKey {
72 pub(crate) fn of(uri: &Uri) -> Self {
73 let text: &str = uri.as_ref();
74 if cfg!(windows) {
75 Self(text.to_ascii_lowercase())
76 } else {
77 Self(text.to_owned())
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84enum Spelling {
85 Canonical,
87 Alias(DiagnosticsKey),
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
93enum Provenance {
94 Pushed,
96 Pulled,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
105struct SlotKey {
106 uri: DiagnosticsKey,
107 provenance: Provenance,
108}
109
110impl SlotKey {
111 const fn pushed(uri: DiagnosticsKey) -> Self {
112 Self {
113 uri,
114 provenance: Provenance::Pushed,
115 }
116 }
117
118 const fn pulled(uri: DiagnosticsKey) -> Self {
119 Self {
120 uri,
121 provenance: Provenance::Pulled,
122 }
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
128struct PullTicket(u64);
129
130#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
133struct ClearEpoch(u64);
134
135#[derive(Debug, Clone, Copy)]
139pub struct PullStamp {
140 ticket: PullTicket,
141 epoch: ClearEpoch,
142 version: DocumentVersion,
143}
144
145impl PullStamp {
146 pub(crate) const fn version(self) -> DocumentVersion {
148 self.version
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub struct DocumentStamp {
156 opening: Option<Opening>,
157 version: DocumentVersion,
158}
159
160impl DocumentStamp {
161 pub const fn new(opening: Option<Opening>, version: DocumentVersion) -> Self {
163 Self { opening, version }
164 }
165
166 pub(crate) const fn opening(self) -> Option<Opening> {
168 self.opening
169 }
170
171 pub(crate) const fn version(self) -> DocumentVersion {
173 self.version
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum VersionCheck {
180 Current,
182 Moved,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum Discard {
189 OlderTicket,
191 ServerCleared,
193 VersionMoved,
195 NoRoom,
197}
198
199#[derive(Debug)]
201pub enum SlotChange {
202 Identical,
204 Replaced { before: Option<DiagnosticSources> },
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum SnapshotBefore {
214 Take,
216 Skip,
218}
219
220#[derive(Debug)]
222struct NoRoom {
223 evicted: Vec<DiagnosticsKey>,
225}
226
227#[derive(Debug)]
229#[must_use]
230pub enum PullWrite {
231 Stored {
233 slot: SlotChange,
234 evicted: Vec<DiagnosticsKey>,
236 },
237 Discarded {
239 reason: Discard,
240 evicted: Vec<DiagnosticsKey>,
242 items: BoundedDiagnostics,
243 },
244}
245
246#[derive(Debug)]
248#[must_use]
249pub enum PushWrite {
250 Stored {
252 evicted: Vec<DiagnosticsKey>,
254 },
255 Dropped {
258 evicted: Vec<DiagnosticsKey>,
260 },
261}
262
263impl PushWrite {
264 #[must_use]
266 pub fn evicted(&self) -> &[DiagnosticsKey] {
267 match self {
268 Self::Stored { evicted } | Self::Dropped { evicted } => evicted,
269 }
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum ChangeOutcome {
276 Changed,
278 Unchanged,
280}
281
282impl ChangeOutcome {
283 pub(crate) fn of(before: Option<&DiagnosticInfo>, after: Option<&DiagnosticInfo>) -> Self {
286 Self::compare(before, after, |d| serde_json::to_vec(d).ok())
287 }
288
289 fn compare(
293 before: Option<&DiagnosticInfo>,
294 after: Option<&DiagnosticInfo>,
295 serialize: impl Fn(&LspDiagnostic) -> Option<Vec<u8>>,
296 ) -> Self {
297 let fingerprint = |info: &DiagnosticInfo| -> Option<Vec<Vec<u8>>> {
298 let mut items = info
299 .diagnostics
300 .iter()
301 .map(&serialize)
302 .collect::<Option<Vec<_>>>()?;
303 items.sort_unstable();
304 Some(items)
305 };
306 match (before, after) {
307 (None, None) => Self::Unchanged,
308 (Some(before), Some(after)) => match (fingerprint(before), fingerprint(after)) {
309 (Some(before), Some(after)) if before == after => Self::Unchanged,
310 _ => Self::Changed,
311 },
312 _ => Self::Changed,
313 }
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum DocumentSync {
320 Synced(DocumentStamp),
322 NotOpen,
324 Unattached,
326}
327
328fn pull_is_superseded(
342 pulled: DocumentStamp,
343 pushed: Option<DocumentVersion>,
344 sync: DocumentSync,
345) -> bool {
346 match (pushed, sync) {
347 (Some(pushed), _) if pushed > pulled.version() => true,
348 (_, DocumentSync::Unattached) => false,
349 (_, DocumentSync::NotOpen) => true,
350 (_, DocumentSync::Synced(current)) => current != pulled,
351 }
352}
353
354#[derive(Debug, Clone, Copy)]
356enum Protect<'a> {
357 Nothing,
358 File(&'a DiagnosticsKey),
359}
360
361#[derive(Debug, Clone, Default, PartialEq, Eq)]
364struct Covered(BTreeSet<usize>);
365
366const MAX_LEARNED_PULL_SOURCES: usize = 32;
368
369const MAX_DIAGNOSTIC_SOURCE_BYTES: usize = 128;
371
372#[derive(Debug, Clone, PartialEq, Eq)]
375struct DiagnosticSource(BoundedText<MAX_DIAGNOSTIC_SOURCE_BYTES>);
376
377impl DiagnosticSource {
378 fn of(diagnostic: &LspDiagnostic) -> Option<Self> {
379 let text = diagnostic.source.as_deref()?;
380 BoundedText::new(text).ok().map(Self)
381 }
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
390enum Freshness {
391 Current,
394 Superseded { covered: Covered },
398}
399
400impl Freshness {
401 fn excludes(&self, index: usize) -> bool {
402 match self {
403 Self::Current => false,
404 Self::Superseded { covered } => covered.0.contains(&index),
405 }
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
412enum Origin {
413 Pushed {
415 spelling: Spelling,
416 version: Option<DocumentVersion>,
418 opening: Option<Opening>,
421 covered: Covered,
424 },
425 Pulled {
428 ticket: PullTicket,
430 state: DocumentStamp,
434 },
435}
436
437impl Origin {
438 const fn version(&self) -> Option<DocumentVersion> {
440 match self {
441 Self::Pushed { version, .. } => *version,
442 Self::Pulled { state, .. } => Some(state.version()),
443 }
444 }
445
446 const fn provenance(&self) -> Provenance {
447 match self {
448 Self::Pushed { .. } => Provenance::Pushed,
449 Self::Pulled { .. } => Provenance::Pulled,
450 }
451 }
452}
453
454#[derive(Debug)]
457struct CachedEntry {
458 uri: Uri,
460 diagnostics: Vec<LspDiagnostic>,
461 owner: ServerId,
463 seq: u64,
465 origin: Origin,
466}
467
468impl CachedEntry {
469 fn info(&self) -> DiagnosticInfo {
471 DiagnosticInfo {
472 uri: self.uri.clone(),
473 version: self.origin.version(),
474 diagnostics: self.diagnostics.clone(),
475 }
476 }
477
478 const fn file<'a>(&'a self, key: &'a SlotKey) -> &'a DiagnosticsKey {
480 match &self.origin {
481 Origin::Pushed {
482 spelling: Spelling::Alias(canonical),
483 ..
484 } => canonical,
485 Origin::Pushed {
486 spelling: Spelling::Canonical,
487 ..
488 }
489 | Origin::Pulled { .. } => &key.uri,
490 }
491 }
492}
493
494pub fn diagnostics_cache_key(uri: &DiagnosticsResourceUri) -> Option<DiagnosticsKey> {
498 let path = crate::bridge::resources::parse_uri(uri.as_str()).ok()?;
499 let lsp_uri = crate::bridge::try_path_to_uri(path.as_path())?;
500 Some(DiagnosticsKey::of(&lsp_uri))
501}
502
503const MAX_SOURCES_PER_FILE: usize = 8;
507
508const MAX_SERVER_MESSAGES: usize = 50;
510
511const MAX_RECENT_EVICTIONS: usize = MAX_DIAGNOSTIC_ENTRIES;
515
516const MAX_EVICTION_MARKS: usize = MAX_DIAGNOSTIC_ENTRIES;
518
519const EVICTION_REPLAY_WINDOW: std::time::Duration = std::time::Duration::from_mins(2);
521
522#[derive(Debug, Default)]
531struct EvictionRecord {
532 at: HashMap<DiagnosticsKey, std::time::Instant>,
533 order: VecDeque<(DiagnosticsKey, std::time::Instant)>,
534 overflow_warn: WarnLimiter,
535}
536
537impl EvictionRecord {
538 fn record(&mut self, file: DiagnosticsKey, now: std::time::Instant) {
539 self.prune_expired(now);
540 self.at.insert(file.clone(), now);
541 self.order.push_back((file, now));
542 while self.at.len() > MAX_RECENT_EVICTIONS {
543 self.forget_oldest(now);
544 }
545 if self.order.len() > self.at.len().saturating_mul(2) {
546 self.compact();
547 }
548 }
549
550 fn contains(&self, key: &DiagnosticsKey, now: std::time::Instant) -> bool {
551 self.at
552 .get(key)
553 .is_some_and(|at| now.saturating_duration_since(*at) <= EVICTION_REPLAY_WINDOW)
554 }
555
556 fn is_live(&self, key: &DiagnosticsKey, at: std::time::Instant) -> bool {
557 self.at.get(key) == Some(&at)
558 }
559
560 fn prune_expired(&mut self, now: std::time::Instant) {
561 while let Some((key, at)) = self
562 .order
563 .pop_front_if(|(_, at)| now.saturating_duration_since(*at) > EVICTION_REPLAY_WINDOW)
564 {
565 if self.is_live(&key, at) {
566 self.at.remove(&key);
567 }
568 }
569 }
570
571 fn forget_oldest(&mut self, now: std::time::Instant) {
572 while let Some((key, at)) = self.order.pop_front() {
573 if self.is_live(&key, at) {
574 self.at.remove(&key);
575 self.warn_overflow(now);
576 return;
577 }
578 }
579 }
580
581 fn warn_overflow(&mut self, now: std::time::Instant) {
582 if self.overflow_warn.due(now, EVICTION_REPLAY_WINDOW) {
583 warn!(
584 "more than {MAX_RECENT_EVICTIONS} diagnostics clears evicted within {}s, \
585 forgetting the oldest; a re-attaching listen may miss them",
586 EVICTION_REPLAY_WINDOW.as_secs()
587 );
588 }
589 }
590
591 fn compact(&mut self) {
592 let mut live: Vec<_> = self.at.iter().map(|(k, at)| (k.clone(), *at)).collect();
593 live.sort_by_key(|(_, at)| *at);
594 self.order = live.into();
595 }
596
597 #[cfg(test)]
598 fn len(&self) -> usize {
599 self.at.len()
600 }
601
602 #[cfg(test)]
603 fn is_empty(&self) -> bool {
604 self.at.is_empty()
605 }
606}
607
608#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610enum EvictedContent {
611 Clean,
613 Lost,
615}
616
617#[derive(Debug)]
618struct EvictionMark {
619 owner: ServerId,
620 content: EvictedContent,
621 seq: u64,
622}
623
624#[derive(Debug, Default)]
635struct EvictionMarks {
636 by_file: HashMap<DiagnosticsKey, EvictionMark>,
637 order: BTreeMap<u64, DiagnosticsKey>,
638 next_seq: u64,
639 overflowed: HashSet<ServerId>,
640}
641
642impl EvictionMarks {
643 fn record(&mut self, file: DiagnosticsKey, owner: ServerId, content: EvictedContent) -> bool {
646 let previous = self.by_file.get(&file).map(|previous| previous.content);
647 let content = match previous {
648 Some(EvictedContent::Lost) => EvictedContent::Lost,
649 _ => content,
650 };
651 let changed = previous != Some(content);
652 self.forget_file(&file);
653 let seq = self.next_seq;
654 self.next_seq = self.next_seq.saturating_add(1);
655 self.order.insert(seq, file.clone());
656 self.by_file.insert(
657 file,
658 EvictionMark {
659 owner,
660 content,
661 seq,
662 },
663 );
664 while self.by_file.len() > MAX_EVICTION_MARKS {
665 let Some((_, oldest)) = self.order.pop_first() else {
666 break;
667 };
668 if let Some(mark) = self.by_file.remove(&oldest) {
669 self.overflowed.insert(mark.owner);
670 }
671 }
672 changed
673 }
674
675 fn forget_file(&mut self, file: &DiagnosticsKey) {
676 if let Some(mark) = self.by_file.remove(file) {
677 self.order.remove(&mark.seq);
678 }
679 }
680
681 fn forget_server(&mut self, server: &ServerId) {
682 self.by_file.retain(|_, mark| &mark.owner != server);
683 self.order.retain(|_, file| self.by_file.contains_key(file));
684 self.overflowed.remove(server);
685 }
686
687 fn content(&self, file: &DiagnosticsKey) -> Option<EvictedContent> {
688 self.by_file.get(file).map(|mark| mark.content)
689 }
690
691 fn has_overflowed(&self, server: &ServerId) -> bool {
692 self.overflowed.contains(server)
693 }
694
695 fn clear_overflow(&mut self, server: &ServerId) -> bool {
698 self.overflowed.remove(server)
699 }
700
701 fn restore_overflow(&mut self, server: &ServerId) {
702 self.overflowed.insert(server.clone());
703 }
704}
705
706pub fn message_as_str(message: &lsp_types::Message) -> &str {
710 match message {
711 lsp_types::Message::String(s) => s,
712 lsp_types::Message::MarkupContent(m) => &m.value,
713 }
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct DiagnosticInfo {
719 pub uri: Uri,
721 pub version: Option<DocumentVersion>,
723 pub diagnostics: Vec<LspDiagnostic>,
725}
726
727#[derive(Debug, Clone, PartialEq, Eq)]
729enum SourceOrigin {
730 Pushed(Spelling),
732 Pulled,
734}
735
736impl SourceOrigin {
737 const fn is_pulled(&self) -> bool {
738 matches!(self, Self::Pulled)
739 }
740}
741
742impl From<&Origin> for SourceOrigin {
743 fn from(origin: &Origin) -> Self {
744 match origin {
745 Origin::Pushed { spelling, .. } => Self::Pushed(spelling.clone()),
746 Origin::Pulled { .. } => Self::Pulled,
747 }
748 }
749}
750
751#[derive(Debug, Clone)]
753struct SourceEntry {
754 info: DiagnosticInfo,
755 origin: SourceOrigin,
756 freshness: Freshness,
757}
758
759#[derive(Debug, Clone)]
767pub struct DiagnosticSources {
768 requested: Uri,
769 entries: Vec<SourceEntry>,
770}
771
772impl DiagnosticSources {
773 pub(crate) fn with_pulled(
776 mut self,
777 file: &Uri,
778 version: Option<DocumentVersion>,
779 diagnostics: BoundedDiagnostics,
780 ) -> Self {
781 self.entries.retain(|entry| !entry.origin.is_pulled());
782 self.entries.push(SourceEntry {
783 info: DiagnosticInfo {
784 uri: file.clone(),
785 version,
786 diagnostics: diagnostics.into_vec(),
787 },
788 origin: SourceOrigin::Pulled,
789 freshness: Freshness::Current,
790 });
791 self
792 }
793
794 #[must_use]
820 pub fn merge(self) -> Option<DiagnosticInfo> {
821 let Self { requested, entries } = self;
822 let has_pulled = entries.iter().any(|entry| entry.origin.is_pulled());
823 if entries.len() <= 1 && !has_pulled {
824 return entries.into_iter().next().map(|entry| entry.info);
825 }
826 let version = entries
827 .iter()
828 .find(|entry| {
829 entry.origin == SourceOrigin::Pushed(Spelling::Canonical)
830 && entry.freshness == Freshness::Current
831 })
832 .and_then(|entry| entry.info.version)
833 .or_else(|| {
834 entries
835 .iter()
836 .find(|entry| entry.origin.is_pulled())
837 .and_then(|entry| entry.info.version)
838 });
839
840 let (pulled, pushed): (Vec<SourceEntry>, Vec<SourceEntry>) = entries
841 .into_iter()
842 .partition(|entry| entry.origin.is_pulled());
843 let pulled: Vec<LspDiagnostic> = pulled
844 .into_iter()
845 .flat_map(|entry| entry.info.diagnostics)
846 .collect();
847 let index = PulledIndex::new(&pulled);
848 let mut seen: HashSet<Vec<u8>> = HashSet::new();
849 let mut kept_pushed = Vec::new();
850 let pushed_items = pushed.into_iter().flat_map(|entry| {
851 let SourceEntry {
852 info, freshness, ..
853 } = entry;
854 info.diagnostics
855 .into_iter()
856 .enumerate()
857 .filter(move |(position, _)| !freshness.excludes(*position))
858 .map(|(_, diagnostic)| diagnostic)
859 });
860 for diagnostic in pushed_items {
861 if index.contains_same_problem(&diagnostic) {
862 continue;
863 }
864 if serde_json::to_vec(&diagnostic).map_or(true, |bytes| seen.insert(bytes)) {
867 kept_pushed.push(diagnostic);
868 }
869 }
870 drop(index);
871 let mut merged = pulled;
872 merged.extend(kept_pushed);
873 merged.sort_by_key(|d| (point(d.range.start), point(d.range.end)));
874 cap_diagnostics_entry_size(&requested, &mut merged);
875
876 Some(DiagnosticInfo {
877 uri: requested,
878 version,
879 diagnostics: merged,
880 })
881 }
882}
883
884#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
886pub struct LogEntry {
887 pub level: LogLevel,
889 pub message: String,
891 pub timestamp: DateTime<Utc>,
893}
894
895#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
897#[serde(rename_all = "lowercase")]
898#[schemars(inline)]
899pub enum LogLevel {
900 Error,
902 Warning,
904 Info,
906 Debug,
908}
909
910impl LogLevel {
911 const fn rank(self) -> u8 {
912 match self {
913 Self::Error => 0,
914 Self::Warning => 1,
915 Self::Info => 2,
916 Self::Debug => 3,
917 }
918 }
919
920 #[must_use]
932 pub const fn meets(self, min: Self) -> bool {
933 self.rank() <= min.rank()
934 }
935}
936
937impl From<lsp_types::MessageType> for LogLevel {
938 fn from(msg_type: lsp_types::MessageType) -> Self {
939 match msg_type {
940 lsp_types::MessageType::Error => Self::Error,
941 lsp_types::MessageType::Warning => Self::Warning,
942 lsp_types::MessageType::Info => Self::Info,
943 _ => Self::Debug,
945 }
946 }
947}
948
949#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
951pub struct ServerMessage {
952 pub message_type: MessageType,
954 pub message: String,
956 pub timestamp: DateTime<Utc>,
958}
959
960#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
962#[serde(rename_all = "lowercase")]
963pub enum MessageType {
964 Error,
966 Warning,
968 Info,
970 Log,
972}
973
974impl From<lsp_types::MessageType> for MessageType {
975 fn from(msg_type: lsp_types::MessageType) -> Self {
976 match msg_type {
977 lsp_types::MessageType::Error => Self::Error,
978 lsp_types::MessageType::Warning => Self::Warning,
979 lsp_types::MessageType::Info => Self::Info,
980 _ => Self::Log,
982 }
983 }
984}
985
986#[derive(Debug)]
988pub struct NotificationCache {
989 entries: HashMap<SlotKey, CachedEntry>,
991 order: HashMap<ServerId, BTreeMap<u64, SlotKey>>,
1003 files: HashMap<DiagnosticsKey, BTreeSet<SlotKey>>,
1006 next_diagnostic_seq: u64,
1011 next_pull_ticket: u64,
1013 pull_sources: HashMap<ServerId, Vec<DiagnosticSource>>,
1017 clear_epochs: HashMap<ServerId, ClearEpoch>,
1020 diagnostics_route_count: Option<usize>,
1035
1036 empty_diagnostics_count: usize,
1046 recent_evictions: EvictionRecord,
1053 eviction_marks: EvictionMarks,
1057 documents: Option<Arc<DocumentTracker>>,
1060 logs: VecDeque<LogEntry>,
1062 messages: VecDeque<ServerMessage>,
1064 push_degraded: HashSet<ServerId>,
1075 indexing: IndexingTracker,
1081}
1082
1083impl Default for NotificationCache {
1084 fn default() -> Self {
1085 Self::new()
1086 }
1087}
1088
1089impl NotificationCache {
1090 #[must_use]
1092 pub fn new() -> Self {
1093 Self {
1094 entries: HashMap::with_capacity(32),
1095 order: HashMap::new(),
1096 files: HashMap::new(),
1097 next_diagnostic_seq: 0,
1098 next_pull_ticket: 0,
1099 pull_sources: HashMap::new(),
1100 clear_epochs: HashMap::new(),
1101 diagnostics_route_count: None,
1102 empty_diagnostics_count: 0,
1103 recent_evictions: EvictionRecord::default(),
1104 eviction_marks: EvictionMarks::default(),
1105 documents: None,
1106 logs: VecDeque::with_capacity(MAX_LOG_ENTRIES),
1107 messages: VecDeque::with_capacity(MAX_SERVER_MESSAGES),
1108 push_degraded: HashSet::new(),
1109 indexing: IndexingTracker::new(),
1110 }
1111 }
1112
1113 pub(crate) fn set_diagnostics_route_count(&mut self, count: usize) {
1130 self.diagnostics_route_count = Some(count.max(1));
1131 }
1132
1133 #[cfg(test)]
1134 pub(crate) const fn configured_route_count(&self) -> Option<usize> {
1135 self.diagnostics_route_count
1136 }
1137
1138 fn per_server_budget(&self) -> usize {
1157 let count = self
1158 .diagnostics_route_count
1159 .unwrap_or_else(|| {
1160 self.order
1161 .values()
1162 .filter(|order| !order.is_empty())
1163 .count()
1164 })
1165 .max(1);
1166 MAX_DIAGNOSTIC_ENTRIES
1167 .checked_div(count)
1168 .unwrap_or(1)
1169 .max(1)
1170 }
1171
1172 fn server_to_evict_from(&self, writer: &ServerId) -> Option<ServerId> {
1196 let largest = self
1197 .order
1198 .iter()
1199 .filter(|(_, order)| !order.is_empty())
1200 .max_by_key(|(id, order)| (order.len(), id.as_str()));
1201
1202 let budget = self.per_server_budget();
1203 if let Some((id, order)) = largest
1204 && order.len() > budget
1205 {
1206 return Some(id.clone());
1207 }
1208
1209 if self
1210 .order
1211 .get(writer)
1212 .is_some_and(|order| !order.is_empty())
1213 {
1214 return Some(writer.clone());
1215 }
1216
1217 largest.map(|(id, _)| id.clone())
1218 }
1219
1220 fn is_empty_entry(&self, key: &SlotKey) -> bool {
1226 self.entries
1227 .get(key)
1228 .is_some_and(|entry| entry.diagnostics.is_empty())
1229 }
1230
1231 fn is_protected(&self, key: &SlotKey, protect: Protect<'_>) -> bool {
1233 match protect {
1234 Protect::Nothing => false,
1235 Protect::File(file) => self
1236 .entries
1237 .get(key)
1238 .is_some_and(|entry| entry.file(key) == file),
1239 }
1240 }
1241
1242 fn oldest_empty_entry_in(
1245 &self,
1246 server: &ServerId,
1247 protect: Protect<'_>,
1248 ) -> Option<(u64, SlotKey)> {
1249 let order = self.order.get(server)?;
1250 order
1251 .iter()
1252 .find(|(_, key)| self.is_empty_entry(key) && !self.is_protected(key, protect))
1253 .map(|(&seq, key)| (seq, key.clone()))
1254 }
1255
1256 fn oldest_entry_in(&self, server: &ServerId, protect: Protect<'_>) -> Option<(u64, SlotKey)> {
1258 self.order
1259 .get(server)?
1260 .iter()
1261 .find(|(_, key)| !self.is_protected(key, protect))
1262 .map(|(&seq, key)| (seq, key.clone()))
1263 }
1264
1265 fn entry_to_evict(
1301 &self,
1302 writer: &ServerId,
1303 protect: Protect<'_>,
1304 ) -> Option<(ServerId, u64, SlotKey)> {
1305 let evict_from = self.server_to_evict_from(writer)?;
1306
1307 if self.empty_diagnostics_count > 0 {
1308 if let Some((seq, key)) = self.oldest_empty_entry_in(&evict_from, protect) {
1309 return Some((evict_from, seq, key));
1310 }
1311
1312 let budget = self.per_server_budget();
1313 let cross_server_pick = self
1314 .order
1315 .iter()
1316 .filter(|(id, order)| order.len() > budget && *id != &evict_from)
1317 .filter_map(|(id, order)| {
1318 self.oldest_empty_entry_in(id, protect)
1319 .map(|(seq, key)| (id, order.len(), seq, key))
1320 })
1321 .max_by_key(|(id, len, ..)| (*len, id.as_str()));
1322
1323 if let Some((id, _, seq, key)) = cross_server_pick {
1324 return Some((id.clone(), seq, key));
1325 }
1326 }
1327
1328 if let Some((seq, key)) = self.oldest_entry_in(&evict_from, protect) {
1329 return Some((evict_from, seq, key));
1330 }
1331 self.order
1332 .iter()
1333 .filter(|(id, _)| *id != &evict_from)
1334 .filter_map(|(id, order)| {
1335 self.oldest_entry_in(id, protect)
1336 .map(|(seq, key)| (id, order.len(), seq, key))
1337 })
1338 .max_by_key(|(id, len, ..)| (*len, id.as_str()))
1339 .map(|(id, _, seq, key)| (id.clone(), seq, key))
1340 }
1341
1342 pub(crate) fn write_published_diagnostics(
1383 &mut self,
1384 server_id: &ServerId,
1385 published: &PublishedDiagnosticsUri,
1386 version: Option<DocumentVersion>,
1387 diagnostics: BoundedDiagnostics,
1388 ) -> PushWrite {
1389 let source_key = SlotKey::pushed(DiagnosticsKey::of(published.source()));
1390 let canonical_key = DiagnosticsKey::of(published.canonical());
1391
1392 let already_indexed = self
1393 .entries
1394 .get(&source_key)
1395 .is_some_and(|entry| entry.file(&source_key) == &canonical_key);
1396 let at_capacity = self.pushed_slot_count(&canonical_key) >= MAX_SOURCES_PER_FILE;
1397 if !already_indexed && at_capacity {
1398 let alias_to_evict = published
1399 .is_canonical()
1400 .then(|| self.oldest_alias_source(&canonical_key))
1401 .flatten();
1402 let Some(oldest_alias) = alias_to_evict else {
1403 debug!(
1404 "dropping diagnostics for {}: {MAX_SOURCES_PER_FILE} published URIs already cached for {}",
1405 published.source().as_ref(),
1406 published.canonical().as_ref()
1407 );
1408 return PushWrite::Dropped {
1409 evicted: Vec::new(),
1410 };
1411 };
1412 self.evict_entry(&oldest_alias);
1413 }
1414
1415 let diagnostics = diagnostics.into_vec();
1416
1417 self.drop_superseded_pull(&canonical_key, published.canonical(), version);
1418
1419 drop(self.take_entry(&source_key));
1423 let evicted = match self.make_room(server_id, Protect::Nothing) {
1424 Ok(evicted) => evicted,
1425 Err(NoRoom { evicted }) => {
1426 debug!(
1427 "dropping diagnostics for {}: no entry could be evicted to make room",
1428 published.source().as_ref()
1429 );
1430 return PushWrite::Dropped { evicted };
1431 }
1432 };
1433
1434 let seq = self.next_seq();
1435 let opening = self.document_opening(published.canonical());
1436 let spelling = if published.is_canonical() {
1437 Spelling::Canonical
1438 } else {
1439 Spelling::Alias(canonical_key)
1440 };
1441 self.insert_entry(
1442 source_key,
1443 CachedEntry {
1444 uri: published.source().clone(),
1445 diagnostics,
1446 owner: server_id.clone(),
1447 seq,
1448 origin: Origin::Pushed {
1449 spelling,
1450 version,
1451 opening,
1452 covered: Covered::default(),
1453 },
1454 },
1455 );
1456 PushWrite::Stored { evicted }
1457 }
1458
1459 #[cfg(test)]
1462 pub(crate) fn store_published_diagnostics(
1463 &mut self,
1464 server_id: &ServerId,
1465 published: &PublishedDiagnosticsUri,
1466 version: Option<i32>,
1467 diagnostics: Vec<LspDiagnostic>,
1468 ) {
1469 drop(self.write_published_diagnostics(
1470 server_id,
1471 published,
1472 version.map(DocumentVersion::new),
1473 BoundedDiagnostics::new(published.source(), diagnostics),
1474 ));
1475 }
1476
1477 const fn next_seq(&mut self) -> u64 {
1478 let seq = self.next_diagnostic_seq;
1479 self.next_diagnostic_seq = self.next_diagnostic_seq.saturating_add(1);
1480 seq
1481 }
1482
1483 fn pushed_slot_count(&self, file: &DiagnosticsKey) -> usize {
1485 self.files.get(file).map_or(0, |slots| {
1486 slots
1487 .iter()
1488 .filter(|slot| slot.provenance == Provenance::Pushed)
1489 .count()
1490 })
1491 }
1492
1493 fn drop_superseded_pull(
1500 &mut self,
1501 file: &DiagnosticsKey,
1502 canonical: &Uri,
1503 pushed_version: Option<DocumentVersion>,
1504 ) {
1505 let slot = SlotKey::pulled(file.clone());
1506 let Some(entry) = self.entries.get(&slot) else {
1507 return;
1508 };
1509 let Origin::Pulled { state: pulled, .. } = entry.origin else {
1510 return;
1511 };
1512 let owner = entry.owner.clone();
1513 let sync = self.document_sync(&owner, canonical);
1514 if pull_is_superseded(pulled, pushed_version, sync) {
1515 debug!(
1516 "dropping the pulled diagnostics of {}: superseded by a push \
1517 (pulled {pulled:?}, pushed {pushed_version:?}, {sync:?})",
1518 canonical.as_ref()
1519 );
1520 drop(self.take_entry(&slot));
1521 }
1522 }
1523
1524 pub(crate) fn pulled_stamp(&self, server: &ServerId, file: &Uri) -> Option<DocumentStamp> {
1530 let entry = self
1531 .entries
1532 .get(&SlotKey::pulled(DiagnosticsKey::of(file)))?;
1533 match entry.origin {
1534 Origin::Pulled { state, .. } if entry.owner == *server => Some(state),
1535 Origin::Pulled { .. } | Origin::Pushed { .. } => None,
1536 }
1537 }
1538
1539 pub(crate) fn attach_documents(&mut self, documents: Arc<DocumentTracker>) {
1543 self.documents = Some(documents);
1544 }
1545
1546 fn document_sync(&self, owner: &ServerId, canonical: &Uri) -> DocumentSync {
1549 let Some(documents) = &self.documents else {
1550 return DocumentSync::Unattached;
1551 };
1552 uri_to_path(canonical)
1553 .and_then(|path| documents.stamp(&path, owner))
1554 .map_or(DocumentSync::NotOpen, DocumentSync::Synced)
1555 }
1556
1557 fn document_opening(&self, canonical: &Uri) -> Option<Opening> {
1560 let path = uri_to_path(canonical)?;
1561 self.documents.as_ref()?.opening(&path)
1562 }
1563
1564 pub(crate) fn begin_pull(&mut self, server: &ServerId, version: DocumentVersion) -> PullStamp {
1570 let ticket = PullTicket(self.next_pull_ticket);
1571 self.next_pull_ticket = self.next_pull_ticket.saturating_add(1);
1572 PullStamp {
1573 ticket,
1574 epoch: self.clear_epochs.get(server).copied().unwrap_or_default(),
1575 version,
1576 }
1577 }
1578
1579 #[cfg(test)]
1591 pub(crate) fn store_pulled_diagnostics(
1592 &mut self,
1593 server_id: &ServerId,
1594 file: &Uri,
1595 stamp: PullStamp,
1596 check: VersionCheck,
1597 items: BoundedDiagnostics,
1598 ) -> PullWrite {
1599 self.store_pulled_diagnostics_with(
1600 server_id,
1601 file,
1602 stamp,
1603 check,
1604 items,
1605 SnapshotBefore::Take,
1606 )
1607 }
1608
1609 pub(crate) fn store_pulled_diagnostics_with(
1613 &mut self,
1614 server_id: &ServerId,
1615 file: &Uri,
1616 stamp: PullStamp,
1617 check: VersionCheck,
1618 items: BoundedDiagnostics,
1619 before: SnapshotBefore,
1620 ) -> PullWrite {
1621 let discard = |reason, items| PullWrite::Discarded {
1622 reason,
1623 evicted: Vec::new(),
1624 items,
1625 };
1626 if stamp.epoch
1627 != self
1628 .clear_epochs
1629 .get(server_id)
1630 .copied()
1631 .unwrap_or_default()
1632 {
1633 return discard(Discard::ServerCleared, items);
1634 }
1635 if check == VersionCheck::Moved {
1636 return discard(Discard::VersionMoved, items);
1637 }
1638 let file_key = DiagnosticsKey::of(file);
1639 let slot_key = SlotKey::pulled(file_key.clone());
1640 if self.entries.get(&slot_key).is_some_and(|stored| {
1641 matches!(stored.origin, Origin::Pulled { ticket, .. } if ticket >= stamp.ticket)
1642 }) {
1643 return discard(Discard::OlderTicket, items);
1644 }
1645
1646 let state = DocumentStamp::new(self.document_opening(file), stamp.version);
1647 let same_state = self.entries.get(&slot_key).is_some_and(|entry| {
1648 entry.diagnostics == items.as_slice()
1649 && matches!(entry.origin, Origin::Pulled { state: stored, .. } if stored == state)
1650 });
1651 let gained = self.coverage_gained(&slot_key);
1652 let slot = if same_state && gained.is_empty() {
1653 SlotChange::Identical
1654 } else {
1655 SlotChange::Replaced {
1656 before: (before == SnapshotBefore::Take).then(|| self.diagnostic_sources(file)),
1657 }
1658 };
1659
1660 drop(self.take_entry_with_coverage(&slot_key, gained));
1661 let evicted = match self.make_room(server_id, Protect::File(&file_key)) {
1662 Ok(evicted) => evicted,
1663 Err(NoRoom { evicted }) => {
1664 return PullWrite::Discarded {
1665 reason: Discard::NoRoom,
1666 evicted,
1667 items,
1668 };
1669 }
1670 };
1671
1672 if !items.as_slice().is_empty() {
1673 self.learn_pull_sources(server_id, items.as_slice());
1674 }
1675 let seq = self.next_seq();
1676 self.insert_entry(
1677 slot_key,
1678 CachedEntry {
1679 uri: file.clone(),
1680 diagnostics: items.into_vec(),
1681 owner: server_id.clone(),
1682 seq,
1683 origin: Origin::Pulled {
1684 ticket: stamp.ticket,
1685 state,
1686 },
1687 },
1688 );
1689 PullWrite::Stored { slot, evicted }
1690 }
1691
1692 #[cfg(test)]
1695 pub(crate) fn store_pulled_for_test(
1696 &mut self,
1697 server_id: &ServerId,
1698 file: &Uri,
1699 diagnostics: Vec<LspDiagnostic>,
1700 ) {
1701 let stamp = self.begin_pull(server_id, DocumentVersion::FIRST);
1702 drop(self.store_pulled_diagnostics(
1703 server_id,
1704 file,
1705 stamp,
1706 VersionCheck::Current,
1707 BoundedDiagnostics::new(file, diagnostics),
1708 ));
1709 }
1710
1711 #[cfg(test)]
1713 pub(crate) fn store_diagnostics(
1714 &mut self,
1715 server_id: &ServerId,
1716 uri: &Uri,
1717 version: Option<i32>,
1718 diagnostics: Vec<LspDiagnostic>,
1719 ) {
1720 let published = PublishedDiagnosticsUri::for_test(uri.clone(), uri.clone());
1721 self.store_published_diagnostics(server_id, &published, version, diagnostics);
1722 }
1723
1724 fn insert_entry(&mut self, key: SlotKey, entry: CachedEntry) {
1728 debug_assert!(!self.entries.contains_key(&key));
1729 debug_assert_eq!(key.provenance, entry.origin.provenance());
1730 self.eviction_marks.forget_file(entry.file(&key));
1731 self.order
1732 .entry(entry.owner.clone())
1733 .or_default()
1734 .insert(entry.seq, key.clone());
1735 self.files
1736 .entry(entry.file(&key).clone())
1737 .or_default()
1738 .insert(key.clone());
1739 if entry.diagnostics.is_empty() {
1740 self.empty_diagnostics_count = self.empty_diagnostics_count.saturating_add(1);
1741 }
1742 self.entries.insert(key, entry);
1743 }
1744
1745 fn take_entry(&mut self, key: &SlotKey) -> Option<CachedEntry> {
1752 let gained = if key.provenance == Provenance::Pulled {
1753 self.coverage_gained(key)
1754 } else {
1755 Vec::new()
1756 };
1757 self.take_entry_with_coverage(key, gained)
1758 }
1759
1760 fn take_entry_with_coverage(
1763 &mut self,
1764 key: &SlotKey,
1765 gained: Vec<(SlotKey, BTreeSet<usize>)>,
1766 ) -> Option<CachedEntry> {
1767 for (pushed, gained) in gained {
1768 if let Some(CachedEntry {
1769 origin: Origin::Pushed { covered, .. },
1770 ..
1771 }) = self.entries.get_mut(&pushed)
1772 {
1773 covered.0.extend(gained);
1774 }
1775 }
1776 let entry = self.entries.remove(key)?;
1777 if let Some(order) = self.order.get_mut(&entry.owner) {
1778 order.remove(&entry.seq);
1779 }
1780 let file = entry.file(key);
1781 if let Some(members) = self.files.get_mut(file) {
1782 members.remove(key);
1783 if members.is_empty() {
1784 self.files.remove(file);
1785 }
1786 }
1787 if entry.diagnostics.is_empty() {
1788 self.empty_diagnostics_count = self.empty_diagnostics_count.saturating_sub(1);
1789 }
1790 Some(entry)
1791 }
1792
1793 fn coverage_gained(&self, slot: &SlotKey) -> Vec<(SlotKey, BTreeSet<usize>)> {
1798 let Some(pulled) = self.entries.get(slot) else {
1799 return Vec::new();
1800 };
1801 let Origin::Pulled { state, .. } = &pulled.origin else {
1802 return Vec::new();
1803 };
1804 let Some(members) = self.files.get(&slot.uri) else {
1805 return Vec::new();
1806 };
1807 let index = PulledIndex::new(&pulled.diagnostics);
1808 members
1809 .iter()
1810 .filter(|key| key.provenance == Provenance::Pushed)
1811 .filter_map(|key| {
1812 let entry = self.entries.get(key)?;
1813 let Origin::Pushed {
1814 opening: pushed_opening,
1815 covered,
1816 ..
1817 } = &entry.origin
1818 else {
1819 return None;
1820 };
1821 let same_state = entry.owner == pulled.owner
1822 && entry.origin.version() == Some(state.version())
1823 && pushed_opening.is_none_or(|pushed| Some(pushed) == state.opening());
1824 if !same_state {
1825 return None;
1826 }
1827 let gained: BTreeSet<usize> = entry
1828 .diagnostics
1829 .iter()
1830 .enumerate()
1831 .filter(|(position, diagnostic)| {
1832 !covered.0.contains(position) && index.contains_same_problem(diagnostic)
1833 })
1834 .map(|(position, _)| position)
1835 .collect();
1836 (!gained.is_empty()).then(|| (key.clone(), gained))
1837 })
1838 .collect()
1839 }
1840
1841 fn learn_pull_sources(&mut self, owner: &ServerId, items: &[LspDiagnostic]) {
1844 let learned = self.pull_sources.entry(owner.clone()).or_default();
1845 for source in items.iter().filter_map(DiagnosticSource::of) {
1846 if learned.len() >= MAX_LEARNED_PULL_SOURCES {
1847 break;
1848 }
1849 if !learned.contains(&source) {
1850 learned.push(source);
1851 }
1852 }
1853 }
1854
1855 fn push_freshness(
1867 &self,
1868 key: &DiagnosticsKey,
1869 requested: &Uri,
1870 entry: &CachedEntry,
1871 ) -> Freshness {
1872 let Origin::Pushed {
1873 opening, covered, ..
1874 } = &entry.origin
1875 else {
1876 return Freshness::Current;
1877 };
1878 let Some(version) = entry.origin.version() else {
1879 return Freshness::Current;
1880 };
1881 let DocumentSync::Synced(synced) = self.document_sync(&entry.owner, requested) else {
1882 return Freshness::Current;
1883 };
1884 let same_opening = opening.is_none_or(|opening| Some(opening) == synced.opening());
1885 if same_opening && version == synced.version() {
1886 return Freshness::Current;
1887 }
1888 let answered_at_synced_state =
1889 self.entries
1890 .get(&SlotKey::pulled(key.clone()))
1891 .is_some_and(|pulled| {
1892 pulled.owner == entry.owner
1893 && matches!(pulled.origin, Origin::Pulled { state, .. } if state == synced)
1894 });
1895 if !answered_at_synced_state {
1896 return Freshness::Current;
1897 }
1898 let mut covered = covered.clone();
1899 if same_opening && let Some(learned) = self.pull_sources.get(&entry.owner) {
1900 covered.0.extend(
1901 entry
1902 .diagnostics
1903 .iter()
1904 .enumerate()
1905 .filter(|(_, diagnostic)| {
1906 DiagnosticSource::of(diagnostic)
1907 .is_some_and(|source| learned.contains(&source))
1908 })
1909 .map(|(position, _)| position),
1910 );
1911 }
1912 Freshness::Superseded { covered }
1913 }
1914
1915 fn make_room(
1923 &mut self,
1924 writer: &ServerId,
1925 protect: Protect<'_>,
1926 ) -> Result<Vec<DiagnosticsKey>, NoRoom> {
1927 let was_overflowed = self.eviction_marks.clear_overflow(writer);
1928 let mut evicted = Vec::new();
1929 while self.entries.len() >= MAX_DIAGNOSTIC_ENTRIES {
1930 let Some((_, _, victim)) = self.entry_to_evict(writer, protect) else {
1931 if was_overflowed {
1932 self.eviction_marks.restore_overflow(writer);
1933 }
1934 return Err(NoRoom { evicted });
1935 };
1936 evicted.extend(self.evict_for_capacity(&victim));
1937 }
1938 Ok(evicted)
1939 }
1940
1941 fn evict_entry(&mut self, key: &SlotKey) {
1945 let Some(entry) = self.take_entry(key) else {
1946 return;
1947 };
1948 if entry.diagnostics.is_empty() {
1949 self.record_empty_eviction(entry.file(key).clone(), std::time::Instant::now());
1950 }
1951 }
1952
1953 fn evict_for_capacity(&mut self, key: &SlotKey) -> Option<DiagnosticsKey> {
1959 let entry = self.entries.get(key)?;
1960 let file = entry.file(key).clone();
1961 let owner = entry.owner.clone();
1962 let content = if entry.diagnostics.is_empty() {
1963 EvictedContent::Clean
1964 } else {
1965 EvictedContent::Lost
1966 };
1967 self.evict_entry(key);
1968 if content == EvictedContent::Lost || !self.files.contains_key(&file) {
1969 self.eviction_marks.record(file.clone(), owner, content);
1970 }
1971 Some(file)
1972 }
1973
1974 fn record_empty_eviction(&mut self, file: DiagnosticsKey, now: std::time::Instant) {
1975 self.recent_evictions.record(file, now);
1976 }
1977
1978 fn was_recently_evicted(&self, uri: &Uri, now: std::time::Instant) -> bool {
1981 let key = DiagnosticsKey::of(uri);
1982 !self.has_diagnostics(uri) && self.recent_evictions.contains(&key, now)
1983 }
1984
1985 pub(crate) fn is_listen_replayable(&self, uri: &Uri) -> bool {
1990 self.has_diagnostics(uri) || self.was_recently_evicted(uri, std::time::Instant::now())
1991 }
1992
1993 #[cfg(test)]
1996 fn assert_consistent(&self) {
1997 let mut ordered = 0;
1998 for (server, order) in &self.order {
1999 for (seq, key) in order {
2000 let entry = self
2001 .entries
2002 .get(key)
2003 .unwrap_or_else(|| panic!("ordered key has no entry"));
2004 assert_eq!((&entry.owner, entry.seq), (server, *seq));
2005 ordered += 1;
2006 }
2007 }
2008 assert_eq!(ordered, self.entries.len(), "order and entries diverge");
2009
2010 let mut filed = 0;
2011 for (file, members) in &self.files {
2012 assert!(!members.is_empty(), "empty file set kept");
2013 for key in members {
2014 let entry = self
2015 .entries
2016 .get(key)
2017 .unwrap_or_else(|| panic!("filed key has no entry"));
2018 assert_eq!(entry.file(key), file);
2019 filed += 1;
2020 }
2021 }
2022 assert_eq!(filed, self.entries.len(), "files and entries diverge");
2023
2024 let empty = self
2025 .entries
2026 .values()
2027 .filter(|entry| entry.diagnostics.is_empty())
2028 .count();
2029 assert_eq!(empty, self.empty_diagnostics_count);
2030
2031 let marks = &self.eviction_marks;
2032 assert_eq!(marks.by_file.len(), marks.order.len(), "marks diverge");
2033 for (seq, file) in &marks.order {
2034 assert_eq!(marks.by_file.get(file).map(|mark| mark.seq), Some(*seq));
2035 }
2036 }
2037
2038 fn oldest_alias_source(&self, canonical_key: &DiagnosticsKey) -> Option<SlotKey> {
2041 self.files
2042 .get(canonical_key)?
2043 .iter()
2044 .filter_map(|source| {
2045 let entry = self.entries.get(source)?;
2046 matches!(
2047 entry.origin,
2048 Origin::Pushed {
2049 spelling: Spelling::Alias(_),
2050 ..
2051 }
2052 )
2053 .then_some((entry.seq, source))
2054 })
2055 .min_by_key(|(seq, _)| *seq)
2056 .map(|(_, source)| source.clone())
2057 }
2058
2059 fn source_keys(&self, key: &DiagnosticsKey) -> BTreeSet<SlotKey> {
2062 let mut keys: BTreeSet<SlotKey> =
2063 self.files.get(key).into_iter().flatten().cloned().collect();
2064 let direct = SlotKey::pushed(key.clone());
2065 if self.entries.contains_key(&direct) {
2066 keys.insert(direct);
2067 }
2068 keys
2069 }
2070
2071 pub(crate) fn store_log(&mut self, level: LogLevel, message: String) {
2076 let entry = LogEntry {
2077 level,
2078 message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
2079 timestamp: Utc::now(),
2080 };
2081
2082 if self.logs.len() >= MAX_LOG_ENTRIES {
2083 self.logs.pop_front();
2084 }
2085 self.logs.push_back(entry);
2086 }
2087
2088 pub(crate) fn store_message(&mut self, message_type: MessageType, message: String) {
2093 let msg = ServerMessage {
2094 message_type,
2095 message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
2096 timestamp: Utc::now(),
2097 };
2098
2099 if self.messages.len() >= MAX_SERVER_MESSAGES {
2100 self.messages.pop_front();
2101 }
2102 self.messages.push_back(msg);
2103 }
2104
2105 pub(crate) fn observe_indexing_signal(
2124 &mut self,
2125 server_id: &ServerId,
2126 method: &str,
2127 params: Option<&serde_json::Value>,
2128 ) {
2129 self.indexing
2130 .observe_server_status(server_id, method, params);
2131 }
2132
2133 pub(crate) fn observe_progress(
2140 &mut self,
2141 server_id: &ServerId,
2142 params: &lsp_types::ProgressParams,
2143 ) {
2144 self.indexing.observe_progress(server_id, params);
2145 }
2146
2147 pub(crate) fn set_indexing_policy(&mut self, server_id: ServerId, policy: IndexingPolicy) {
2150 self.indexing.set_policy(server_id, policy);
2151 }
2152
2153 #[must_use]
2166 pub fn indexing_state(&self, server_id: &ServerId) -> IndexingState {
2167 self.indexing.state(server_id)
2168 }
2169
2170 pub(crate) fn reset_indexing_state(&mut self, server_id: &ServerId, reset: IndexingReset) {
2183 self.indexing.reset(server_id, reset);
2184 }
2185
2186 #[inline]
2199 #[must_use]
2200 pub fn diagnostics(&self, uri: &Uri) -> Option<DiagnosticInfo> {
2201 self.entries
2202 .get(&SlotKey::pushed(DiagnosticsKey::of(uri)))
2203 .map(CachedEntry::info)
2204 }
2205
2206 #[must_use]
2225 pub fn diagnostic_sources(&self, uri: &Uri) -> DiagnosticSources {
2226 let key = DiagnosticsKey::of(uri);
2227 let entries = self
2228 .source_keys(&key)
2229 .into_iter()
2230 .filter_map(|source| {
2231 let entry = self.entries.get(&source)?;
2232 Some(SourceEntry {
2233 info: entry.info(),
2234 origin: SourceOrigin::from(&entry.origin),
2235 freshness: self.push_freshness(&key, uri, entry),
2236 })
2237 })
2238 .collect();
2239 DiagnosticSources {
2240 requested: uri.clone(),
2241 entries,
2242 }
2243 }
2244
2245 #[must_use]
2259 pub fn has_diagnostics(&self, uri: &Uri) -> bool {
2260 let key = DiagnosticsKey::of(uri);
2261 self.entries.contains_key(&SlotKey::pushed(key.clone())) || self.files.contains_key(&key)
2262 }
2263
2264 #[must_use]
2285 pub fn availability(&self, uri: &Uri, route: Option<&ServerId>) -> DiagnosticsAvailability {
2286 if self.has_diagnostics(uri) {
2287 return DiagnosticsAvailability::Published;
2288 }
2289 match self.eviction_marks.content(&DiagnosticsKey::of(uri)) {
2290 Some(EvictedContent::Clean) => DiagnosticsAvailability::Published,
2291 Some(EvictedContent::Lost) => DiagnosticsAvailability::Evicted,
2292 None if route.is_some_and(|server| self.eviction_marks.has_overflowed(server)) => {
2293 DiagnosticsAvailability::Evicted
2294 }
2295 None => DiagnosticsAvailability::Pending,
2296 }
2297 }
2298
2299 #[inline]
2304 #[must_use]
2305 pub fn diagnostics_owner(&self, uri: &Uri) -> Option<&ServerId> {
2306 let key = DiagnosticsKey::of(uri);
2307 self.entries
2308 .get(&SlotKey::pushed(key.clone()))
2309 .or_else(|| {
2310 let slots = self.files.get(&key)?;
2311 slots
2312 .iter()
2313 .filter(|slot| slot.provenance == Provenance::Pushed)
2314 .chain(slots.iter())
2315 .find_map(|source| self.entries.get(source))
2316 })
2317 .map(|entry| &entry.owner)
2318 }
2319
2320 #[inline]
2322 #[must_use]
2323 pub const fn logs(&self) -> &VecDeque<LogEntry> {
2324 &self.logs
2325 }
2326
2327 #[inline]
2329 #[must_use]
2330 pub const fn messages(&self) -> &VecDeque<ServerMessage> {
2331 &self.messages
2332 }
2333
2334 pub(crate) fn clear_server_diagnostics(&mut self, server_id: &ServerId) -> Vec<DiagnosticsKey> {
2344 let epoch = self.clear_epochs.entry(server_id.clone()).or_default();
2345 epoch.0 = epoch.0.saturating_add(1);
2346 self.eviction_marks.forget_server(server_id);
2347 self.pull_sources.remove(server_id);
2348 let Some(order) = self.order.remove(server_id) else {
2349 return Vec::new();
2350 };
2351 let mut cleared = Vec::with_capacity(order.len());
2352 for key in order.into_values() {
2353 if let Some(entry) = self.entries.get(&key) {
2354 cleared.push(entry.file(&key).clone());
2355 }
2356 self.evict_entry(&key);
2357 }
2358 cleared.sort_unstable();
2359 cleared.dedup();
2360 cleared
2361 }
2362
2363 pub(crate) fn record_lost_files(
2378 &mut self,
2379 owner: &ServerId,
2380 published: &[PublishedDiagnosticsUri],
2381 ) -> Vec<DiagnosticsKey> {
2382 let mut changed = BTreeSet::new();
2383 for publication in published {
2384 let file = self.lost_file(publication);
2385 let stale: Vec<SlotKey> = self
2386 .files
2387 .get(&file)
2388 .into_iter()
2389 .flatten()
2390 .filter(|key| {
2391 key.provenance == Provenance::Pushed
2392 && self
2393 .entries
2394 .get(key)
2395 .is_some_and(|entry| entry.owner == *owner)
2396 })
2397 .cloned()
2398 .collect();
2399 let evicted = !stale.is_empty();
2400 for key in stale {
2401 self.evict_entry(&key);
2402 }
2403 let marked = !self.has_diagnostics_for(&file)
2404 && self
2405 .eviction_marks
2406 .record(file.clone(), owner.clone(), EvictedContent::Lost);
2407 if evicted || marked {
2408 changed.insert(file);
2409 }
2410 }
2411 changed.into_iter().collect()
2412 }
2413
2414 fn lost_file(&self, published: &PublishedDiagnosticsUri) -> DiagnosticsKey {
2416 let by_source = || {
2417 let key = SlotKey::pushed(DiagnosticsKey::of(published.source()));
2418 self.entries.get(&key).map(|entry| entry.file(&key).clone())
2419 };
2420 (published.form() == CanonicalForm::Fallback)
2421 .then(by_source)
2422 .flatten()
2423 .unwrap_or_else(|| DiagnosticsKey::of(published.canonical()))
2424 }
2425
2426 pub(crate) fn record_unnamed_loss(&mut self, owner: &ServerId) {
2429 self.eviction_marks.restore_overflow(owner);
2430 }
2431
2432 fn has_diagnostics_for(&self, key: &DiagnosticsKey) -> bool {
2434 self.files.contains_key(key) || self.entries.contains_key(&SlotKey::pushed(key.clone()))
2435 }
2436
2437 pub(crate) fn mark_push_degraded(&mut self, server_id: &ServerId) {
2441 self.push_degraded.insert(server_id.clone());
2442 }
2443
2444 pub(crate) fn clear_push_degraded(&mut self, server_id: &ServerId) {
2447 self.push_degraded.remove(server_id);
2448 }
2449
2450 #[inline]
2456 #[must_use]
2457 pub(crate) fn is_push_degraded(&self, server_id: &ServerId) -> bool {
2458 self.push_degraded.contains(server_id)
2459 }
2460
2461 #[inline]
2463 #[must_use]
2464 #[cfg(test)]
2465 pub(crate) fn diagnostics_count(&self) -> usize {
2466 self.entries.len()
2467 }
2468
2469 #[inline]
2471 #[must_use]
2472 #[cfg(test)]
2473 pub(crate) fn logs_count(&self) -> usize {
2474 self.logs.len()
2475 }
2476
2477 #[inline]
2479 #[must_use]
2480 #[cfg(test)]
2481 pub(crate) fn messages_count(&self) -> usize {
2482 self.messages.len()
2483 }
2484}
2485
2486pub fn apply_lifecycle_notification(
2523 cache: &mut NotificationCache,
2524 server_id: &ServerId,
2525 notif: crate::lsp::LspNotification,
2526) {
2527 match notif {
2528 crate::lsp::LspNotification::Progress(params) => {
2529 cache.observe_progress(server_id, ¶ms);
2530 }
2531 crate::lsp::LspNotification::Other { method, params } => {
2532 cache.observe_indexing_signal(server_id, &method, params.as_ref());
2533 }
2534 crate::lsp::LspNotification::PublishDiagnostics(_)
2535 | crate::lsp::LspNotification::LogMessage(_)
2536 | crate::lsp::LspNotification::ShowMessage(_) => {}
2537 }
2538}
2539
2540pub async fn on_lifecycle(
2546 cache: Option<&tokio::sync::Mutex<NotificationCache>>,
2547 server_id: &ServerId,
2548 pinned_tsserver: Option<&std::path::Path>,
2549 notif: crate::lsp::LspNotification,
2550) {
2551 crate::lsp::tsserver_pin::warn_if_pin_ignored(pinned_tsserver, ¬if, server_id);
2552 if let Some(cache) = cache {
2553 apply_lifecycle_notification(&mut *cache.lock().await, server_id, notif);
2554 }
2555}
2556
2557#[cfg(test)]
2558mod pull_tests;
2559
2560#[cfg(test)]
2561mod tests {
2562 use lsp_types::{Position, Range};
2563
2564 use super::bounds::*;
2565 use super::*;
2566 use crate::bridge::indexing::IndexingReset;
2567 use crate::test_lsp::CapturedLogs;
2568
2569 fn test_server() -> ServerId {
2573 ServerId::from_static("test-server")
2574 }
2575
2576 #[test]
2577 fn test_notification_cache_new() {
2578 let cache = NotificationCache::new();
2579 assert_eq!(cache.diagnostics_count(), 0);
2580 assert_eq!(cache.logs_count(), 0);
2581 assert_eq!(cache.messages_count(), 0);
2582 }
2583
2584 #[test]
2585 fn test_store_and_diagnostics() {
2586 let mut cache = NotificationCache::new();
2587 let uri: Uri = Uri::from("file:///test.rs");
2588
2589 let diagnostic = LspDiagnostic {
2590 range: Range {
2591 start: Position {
2592 line: 0,
2593 character: 0,
2594 },
2595 end: Position {
2596 line: 0,
2597 character: 5,
2598 },
2599 },
2600 severity: Some(lsp_types::DiagnosticSeverity::Error),
2601 message: "test error".to_string().into(),
2602 code: None,
2603 source: None,
2604 code_description: None,
2605 related_information: None,
2606 tags: None,
2607 data: None,
2608 };
2609
2610 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2611
2612 let stored = cache.diagnostics(&uri).unwrap();
2613 assert_eq!(stored.uri, uri);
2614 assert_eq!(stored.version.map(DocumentVersion::get), Some(1));
2615 assert_eq!(stored.diagnostics.len(), 1);
2616 assert_eq!(
2617 stored.diagnostics[0].message,
2618 lsp_types::Message::String("test error".to_string())
2619 );
2620 }
2621
2622 #[test]
2625 fn test_store_diagnostics_truncates_oversized_message() {
2626 let mut cache = NotificationCache::new();
2627 let uri: Uri = Uri::from("file:///test.rs");
2628 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
2629
2630 let diagnostic = LspDiagnostic {
2631 range: Range {
2632 start: Position {
2633 line: 0,
2634 character: 0,
2635 },
2636 end: Position {
2637 line: 0,
2638 character: 5,
2639 },
2640 },
2641 severity: Some(lsp_types::DiagnosticSeverity::Error),
2642 message: oversized.clone().into(),
2643 code: None,
2644 source: None,
2645 code_description: None,
2646 related_information: None,
2647 tags: None,
2648 data: None,
2649 };
2650
2651 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2652
2653 let stored = cache.diagnostics(&uri).unwrap();
2654 let stored = message_as_str(&stored.diagnostics[0].message);
2655 assert!(stored.len() < oversized.len());
2656 assert!(stored.ends_with("... (truncated)"));
2657 }
2658
2659 fn minimal_diagnostic(message: String) -> LspDiagnostic {
2662 LspDiagnostic {
2663 range: Range {
2664 start: Position {
2665 line: 0,
2666 character: 0,
2667 },
2668 end: Position {
2669 line: 0,
2670 character: 5,
2671 },
2672 },
2673 severity: Some(lsp_types::DiagnosticSeverity::Error),
2674 message: message.into(),
2675 code: None,
2676 source: None,
2677 code_description: None,
2678 related_information: None,
2679 tags: None,
2680 data: None,
2681 }
2682 }
2683
2684 #[test]
2687 fn test_reported_severity_orders_most_severe_first_and_defaults_to_information() {
2688 let mut severityless = minimal_diagnostic("none".to_string());
2689 severityless.severity = None;
2690 let mut hint = minimal_diagnostic("hint".to_string());
2691 hint.severity = Some(lsp_types::DiagnosticSeverity::Hint);
2692
2693 assert_eq!(
2694 ReportedSeverity::of(&severityless),
2695 ReportedSeverity::Information
2696 );
2697 assert!(ReportedSeverity::of(&severityless) < ReportedSeverity::of(&hint));
2698 assert!(ReportedSeverity::Error < ReportedSeverity::Warning);
2699 assert!(ReportedSeverity::Warning < ReportedSeverity::Information);
2700 }
2701
2702 #[test]
2706 fn test_store_diagnostics_caps_aggregate_size_for_many_small_diagnostics() {
2707 let mut cache = NotificationCache::new();
2708 let uri: Uri = Uri::from("file:///test.rs");
2709
2710 let diagnostics: Vec<LspDiagnostic> = (0..5000)
2714 .map(|i| {
2715 minimal_diagnostic(format!(
2716 "diagnostic number {i}, padded: {}",
2717 "x".repeat(200)
2718 ))
2719 })
2720 .collect();
2721 let original_count = diagnostics.len();
2722
2723 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
2724
2725 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2726 assert!(
2727 stored.len() < original_count,
2728 "aggregate cap must trim the list, kept {} of {original_count}",
2729 stored.len()
2730 );
2731 assert!(!stored.is_empty(), "must keep at least one diagnostic");
2732 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2733 assert!(
2734 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2735 "stored entry must fit the aggregate cap, got {serialized_len} bytes"
2736 );
2737 }
2738
2739 #[test]
2743 fn test_store_diagnostics_truncation_keeps_largest_fitting_prefix() {
2744 let mut cache = NotificationCache::new();
2745 let uri: Uri = Uri::from("file:///test.rs");
2746
2747 let diagnostics: Vec<LspDiagnostic> = (0..5000)
2751 .map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
2752 .collect();
2753
2754 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
2755
2756 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2757 assert!(
2758 stored.len() > 2600,
2759 "largest-fitting-prefix search must keep far more than half, kept {}",
2760 stored.len()
2761 );
2762 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2763 assert!(serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES);
2764 let mut with_one_more = stored.clone();
2768 with_one_more.push(minimal_diagnostic(format!(
2769 "diagnostic overflow: {}",
2770 "x".repeat(250)
2771 )));
2772 assert!(
2773 serde_json::to_vec(&with_one_more).unwrap().len() > MAX_DIAGNOSTICS_ENTRY_BYTES,
2774 "kept count must be the largest that fits, not merely a fitting count"
2775 );
2776 }
2777
2778 #[test]
2782 fn test_store_diagnostics_truncation_prefers_higher_severity() {
2783 let mut cache = NotificationCache::new();
2784 let uri: Uri = Uri::from("file:///test.rs");
2785
2786 let mut diagnostics: Vec<LspDiagnostic> = (0..5000)
2787 .map(|i| {
2788 let mut d = minimal_diagnostic(format!("hint {i}: {}", "x".repeat(200)));
2789 d.severity = Some(lsp_types::DiagnosticSeverity::Hint);
2790 d
2791 })
2792 .collect();
2793 let mut trailing_error = minimal_diagnostic("the one real error".to_string());
2794 trailing_error.severity = Some(lsp_types::DiagnosticSeverity::Error);
2795 diagnostics.push(trailing_error);
2796
2797 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
2798
2799 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2800 assert!(
2801 stored
2802 .iter()
2803 .any(|d| message_as_str(&d.message) == "the one real error"),
2804 "the trailing ERROR diagnostic must survive truncation over leading HINT noise"
2805 );
2806 }
2807
2808 #[test]
2812 fn test_store_diagnostics_warns_when_truncating_list() {
2813 use tracing_subscriber::layer::SubscriberExt as _;
2814
2815 let mut cache = NotificationCache::new();
2816 let uri: Uri = Uri::from("file:///test.rs");
2817 let diagnostics: Vec<LspDiagnostic> = (0..5000)
2818 .map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
2819 .collect();
2820
2821 let captured = CapturedLogs::default();
2822 let subscriber = tracing_subscriber::registry().with(captured.clone());
2823 let guard = tracing::subscriber::set_default(subscriber);
2824 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
2825 drop(guard);
2826
2827 let messages = captured.messages();
2828 assert!(
2829 messages
2830 .iter()
2831 .any(|m| m.contains("highest-severity") && m.contains("file:///test.rs")),
2832 "expected a truncation warning naming the URI, got: {messages:?}"
2833 );
2834 }
2835
2836 #[test]
2840 fn test_store_diagnostics_warns_when_dropping_data_blob() {
2841 use tracing_subscriber::layer::SubscriberExt as _;
2842
2843 let mut cache = NotificationCache::new();
2844 let uri: Uri = Uri::from("file:///test.rs");
2845 let mut diagnostic = minimal_diagnostic("small message".to_string());
2846 diagnostic.data = Some(serde_json::json!({
2847 "blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
2848 }));
2849
2850 let captured = CapturedLogs::default();
2851 let subscriber = tracing_subscriber::registry().with(captured.clone());
2852 let guard = tracing::subscriber::set_default(subscriber);
2853 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2854 drop(guard);
2855
2856 let messages = captured.messages();
2857 assert!(
2858 messages.iter().any(|m| m.contains("code-action")),
2859 "expected a warning noting the code-action quick-fix impact, got: {messages:?}"
2860 );
2861 }
2862
2863 #[test]
2868 fn test_store_diagnostics_drops_oversized_data_blob_on_single_diagnostic() {
2869 let mut cache = NotificationCache::new();
2870 let uri: Uri = Uri::from("file:///test.rs");
2871
2872 let mut diagnostic = minimal_diagnostic("small message".to_string());
2873 diagnostic.data = Some(serde_json::json!({
2874 "blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
2875 }));
2876
2877 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2878
2879 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2880 assert_eq!(stored.len(), 1);
2881 assert_eq!(
2882 stored[0].message,
2883 lsp_types::Message::String("small message".to_string())
2884 );
2885 assert!(
2886 stored[0].data.is_none(),
2887 "oversized data blob must be dropped"
2888 );
2889 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2890 assert!(
2891 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2892 "stored entry must fit the aggregate cap after dropping data, got {serialized_len} bytes"
2893 );
2894 }
2895
2896 #[test]
2901 fn test_store_diagnostics_truncates_oversized_source_on_single_diagnostic() {
2902 let mut cache = NotificationCache::new();
2903 let uri: Uri = Uri::from("file:///test.rs");
2904
2905 let mut diagnostic = minimal_diagnostic("small message".to_string());
2906 diagnostic.source = Some("x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000));
2907
2908 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2909
2910 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2911 assert_eq!(stored.len(), 1);
2912 assert_eq!(
2913 stored[0].message,
2914 lsp_types::Message::String("small message".to_string())
2915 );
2916 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2917 assert!(
2918 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2919 "stored entry must fit the aggregate cap after truncating source, got {serialized_len} bytes"
2920 );
2921 }
2922
2923 #[test]
2930 fn test_store_diagnostics_caps_single_diagnostic_with_every_field_maxed_out() {
2931 let mut cache = NotificationCache::new();
2932 let uri: Uri = Uri::from("file:///test.rs");
2933
2934 let mut diagnostic = minimal_diagnostic("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
2939 diagnostic.source = Some("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
2940 diagnostic.code = Some(lsp_types::Code::String(
2941 "x".repeat(MAX_ENTRY_TEXT_BYTES + 1000),
2942 ));
2943 diagnostic.data = Some(serde_json::json!({ "blob": "x".repeat(MAX_ENTRY_TEXT_BYTES) }));
2944 diagnostic.tags = Some(vec![lsp_types::DiagnosticTag::Unnecessary; 50]);
2945 diagnostic.related_information = Some(vec![
2946 lsp_types::DiagnosticRelatedInformation {
2947 location: lsp_types::Location {
2948 uri: uri.clone(),
2949 range: Range::default(),
2950 },
2951 message: "x".repeat(1000),
2952 };
2953 5
2954 ]);
2955
2956 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2957
2958 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2959 assert_eq!(stored.len(), 1);
2960 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2961 assert!(
2962 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2963 "postcondition must hold even with every field maxed out, got {serialized_len} bytes"
2964 );
2965 }
2966
2967 #[test]
2976 fn test_cap_diagnostics_entry_size_terminal_fallback_bounds_untruncated_message() {
2977 let uri: Uri = Uri::from("file:///test.rs");
2978 let mut diagnostics = vec![minimal_diagnostic(
2979 "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
2980 )];
2981
2982 cap_diagnostics_entry_size(&uri, &mut diagnostics);
2983
2984 assert_eq!(diagnostics.len(), 1);
2985 assert!(
2986 message_as_str(&diagnostics[0].message).len()
2987 <= DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES + 20,
2988 "terminal fallback must truncate the message itself, got {} bytes",
2989 message_as_str(&diagnostics[0].message).len()
2990 );
2991 let serialized_len = serde_json::to_vec(&diagnostics).unwrap().len();
2992 assert!(
2993 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2994 "postcondition must hold via the terminal fallback, got {serialized_len} bytes"
2995 );
2996 }
2997
2998 fn serialized_len_of(diagnostics: &[LspDiagnostic]) -> usize {
2999 serde_json::to_vec(diagnostics).unwrap().len()
3000 }
3001
3002 #[test]
3005 fn test_cap_size_of_an_estimable_list_is_the_estimate() {
3006 let uri: Uri = Uri::from("file:///test.rs");
3007 let mut diagnostics = vec![minimal_diagnostic("short".to_owned()); 3];
3008
3009 let size = cap_diagnostics_entry_size(&uri, &mut diagnostics);
3010
3011 assert_eq!(diagnostics.len(), 3);
3012 assert!(size >= serialized_len_of(&diagnostics), "{size}");
3013 assert!(size <= MAX_DIAGNOSTICS_ENTRY_BYTES);
3014 }
3015
3016 #[test]
3019 fn test_cap_size_of_a_fitting_list_with_data_is_its_serialized_length() {
3020 let uri: Uri = Uri::from("file:///test.rs");
3021 let mut diagnostics: Vec<LspDiagnostic> = (0..50)
3022 .map(|n| {
3023 let mut diagnostic = minimal_diagnostic(format!("d{n}"));
3024 diagnostic.data = Some(serde_json::json!({ "n": n }));
3025 diagnostic
3026 })
3027 .collect();
3028
3029 let size = cap_diagnostics_entry_size(&uri, &mut diagnostics);
3030
3031 assert_eq!(diagnostics.len(), 50);
3032 assert_eq!(size, serialized_len_of(&diagnostics));
3033 }
3034
3035 #[test]
3038 fn test_cap_size_after_cutting_the_list_is_the_final_serialized_length() {
3039 let uri: Uri = Uri::from("file:///test.rs");
3040 let blob = "x".repeat(MAX_ENTRY_TEXT_BYTES / 2);
3041 let mut diagnostics: Vec<LspDiagnostic> = (0..8)
3042 .map(|n| {
3043 let mut diagnostic = minimal_diagnostic(format!("d{n}"));
3044 diagnostic.data = Some(serde_json::json!({ "blob": blob }));
3045 diagnostic
3046 })
3047 .collect();
3048
3049 let size = cap_diagnostics_entry_size(&uri, &mut diagnostics);
3050
3051 assert!(diagnostics.len() < 8);
3052 assert_eq!(size, serialized_len_of(&diagnostics));
3053 assert!(size <= MAX_DIAGNOSTICS_ENTRY_BYTES);
3054 }
3055
3056 #[test]
3059 fn test_cap_size_after_dropping_opaque_fields_is_the_final_serialized_length() {
3060 let uri: Uri = Uri::from("file:///test.rs");
3061 let mut diagnostic = minimal_diagnostic("only".to_owned());
3062 diagnostic.data =
3063 Some(serde_json::json!({ "blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES) }));
3064 let mut diagnostics = vec![diagnostic];
3065
3066 let size = cap_diagnostics_entry_size(&uri, &mut diagnostics);
3067
3068 assert!(diagnostics[0].data.is_none());
3069 assert_eq!(size, serialized_len_of(&diagnostics));
3070 }
3071
3072 #[test]
3074 fn test_cap_size_after_the_terminal_fallback_is_the_final_serialized_length() {
3075 let uri: Uri = Uri::from("file:///test.rs");
3076 let mut diagnostics = vec![minimal_diagnostic(
3077 "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
3078 )];
3079
3080 let size = cap_diagnostics_entry_size(&uri, &mut diagnostics);
3081
3082 assert_eq!(size, serialized_len_of(&diagnostics));
3083 assert!(size <= MAX_DIAGNOSTICS_ENTRY_BYTES);
3084 }
3085
3086 #[test]
3089 fn test_bounded_diagnostics_records_the_size_it_measured() {
3090 let uri: Uri = Uri::from("file:///test.rs");
3091 let diagnostics: Vec<LspDiagnostic> = (0..1000)
3092 .map(|n| {
3093 let mut diagnostic = minimal_diagnostic(format!("d{n}"));
3094 diagnostic.data = Some(serde_json::json!({ "n": n }));
3095 diagnostic
3096 })
3097 .collect();
3098
3099 let bounded = BoundedDiagnostics::new(&uri, diagnostics);
3100
3101 assert_eq!(
3102 bounded.buffered_bytes(),
3103 serialized_len_of(bounded.as_slice())
3104 );
3105 }
3106
3107 #[test]
3112 fn test_store_diagnostics_cheap_path_leaves_small_diagnostics_untouched() {
3113 let mut cache = NotificationCache::new();
3114 let uri: Uri = Uri::from("file:///test.rs");
3115
3116 let mut diagnostic = minimal_diagnostic("a small, ordinary diagnostic message".to_string());
3117 diagnostic.source = Some("rustc".to_string());
3118
3119 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
3120
3121 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
3122 assert_eq!(stored.len(), 1);
3123 assert_eq!(
3124 stored[0].message,
3125 lsp_types::Message::String("a small, ordinary diagnostic message".to_string())
3126 );
3127 assert_eq!(stored[0].source.as_deref(), Some("rustc"));
3128 }
3129
3130 #[test]
3140 fn test_store_diagnostics_cheap_path_escape_safe_for_control_character_heavy_message() {
3141 let mut cache = NotificationCache::new();
3142 let uri: Uri = Uri::from("file:///test.rs");
3143
3144 let nul_heavy_message = "\0".repeat(MAX_ENTRY_TEXT_BYTES);
3145 let diagnostics: Vec<LspDiagnostic> = (0..3)
3146 .map(|_| minimal_diagnostic(nul_heavy_message.clone()))
3147 .collect();
3148
3149 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
3150
3151 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
3152 let serialized_len = serde_json::to_vec(stored).unwrap().len();
3153 assert!(
3154 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
3155 "escape-heavy content must not let the cheap-estimate fast path skip the real cap, \
3156 got {serialized_len} bytes"
3157 );
3158 }
3159
3160 #[test]
3161 fn test_store_diagnostics_replaces_existing() {
3162 let mut cache = NotificationCache::new();
3163 let uri: Uri = Uri::from("file:///test.rs");
3164
3165 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3166 assert_eq!(cache.diagnostics_count(), 1);
3167
3168 cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
3169 assert_eq!(cache.diagnostics_count(), 1);
3170
3171 let stored = cache.diagnostics(&uri).unwrap();
3172 assert_eq!(stored.version.map(DocumentVersion::get), Some(2));
3173 }
3174
3175 #[test]
3176 fn test_store_and_get_logs() {
3177 let mut cache = NotificationCache::new();
3178
3179 cache.store_log(LogLevel::Error, "error message".to_string());
3180 cache.store_log(LogLevel::Info, "info message".to_string());
3181
3182 let logs = cache.logs();
3183 assert_eq!(logs.len(), 2);
3184 assert_eq!(logs[0].level, LogLevel::Error);
3185 assert_eq!(logs[0].message, "error message");
3186 assert_eq!(logs[1].level, LogLevel::Info);
3187 assert_eq!(logs[1].message, "info message");
3188 }
3189
3190 #[test]
3191 fn test_logs_max_capacity() {
3192 let mut cache = NotificationCache::new();
3193
3194 for i in 0..MAX_LOG_ENTRIES + 10 {
3196 cache.store_log(LogLevel::Info, format!("message {i}"));
3197 }
3198
3199 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
3200
3201 let logs = cache.logs();
3203 assert_eq!(logs.front().unwrap().message, "message 10");
3204 assert_eq!(
3205 logs.back().unwrap().message,
3206 format!("message {}", MAX_LOG_ENTRIES + 9)
3207 );
3208 }
3209
3210 #[test]
3214 fn test_store_log_truncates_oversized_message() {
3215 let mut cache = NotificationCache::new();
3216 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
3217
3218 cache.store_log(LogLevel::Info, oversized.clone());
3219
3220 let stored = &cache.logs()[0].message;
3221 assert!(stored.len() < oversized.len());
3222 assert!(stored.ends_with("... (truncated)"));
3223 }
3224
3225 #[test]
3226 fn test_store_log_does_not_truncate_message_at_or_below_limit() {
3227 let mut cache = NotificationCache::new();
3228 let message = "a".repeat(MAX_ENTRY_TEXT_BYTES);
3229
3230 cache.store_log(LogLevel::Info, message.clone());
3231
3232 assert_eq!(cache.logs()[0].message, message);
3233 }
3234
3235 #[test]
3236 fn test_store_and_get_messages() {
3237 let mut cache = NotificationCache::new();
3238
3239 cache.store_message(MessageType::Error, "error msg".to_string());
3240 cache.store_message(MessageType::Warning, "warning msg".to_string());
3241
3242 let messages = cache.messages();
3243 assert_eq!(messages.len(), 2);
3244 assert_eq!(messages[0].message_type, MessageType::Error);
3245 assert_eq!(messages[0].message, "error msg");
3246 assert_eq!(messages[1].message_type, MessageType::Warning);
3247 assert_eq!(messages[1].message, "warning msg");
3248 }
3249
3250 #[test]
3251 fn test_messages_max_capacity() {
3252 let mut cache = NotificationCache::new();
3253
3254 for i in 0..MAX_SERVER_MESSAGES + 10 {
3256 cache.store_message(MessageType::Info, format!("message {i}"));
3257 }
3258
3259 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
3260
3261 let messages = cache.messages();
3263 assert_eq!(messages.front().unwrap().message, "message 10");
3264 assert_eq!(
3265 messages.back().unwrap().message,
3266 format!("message {}", MAX_SERVER_MESSAGES + 9)
3267 );
3268 }
3269
3270 #[test]
3272 fn test_store_message_truncates_oversized_message() {
3273 let mut cache = NotificationCache::new();
3274 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
3275
3276 cache.store_message(MessageType::Info, oversized.clone());
3277
3278 let stored = &cache.messages()[0].message;
3279 assert!(stored.len() < oversized.len());
3280 assert!(stored.ends_with("... (truncated)"));
3281 }
3282
3283 #[test]
3284 fn test_log_levels() {
3285 let mut cache = NotificationCache::new();
3286
3287 cache.store_log(LogLevel::Error, "error".to_string());
3288 cache.store_log(LogLevel::Warning, "warning".to_string());
3289 cache.store_log(LogLevel::Info, "info".to_string());
3290 cache.store_log(LogLevel::Debug, "debug".to_string());
3291
3292 let logs = cache.logs();
3293 assert_eq!(logs[0].level, LogLevel::Error);
3294 assert_eq!(logs[1].level, LogLevel::Warning);
3295 assert_eq!(logs[2].level, LogLevel::Info);
3296 assert_eq!(logs[3].level, LogLevel::Debug);
3297 }
3298
3299 #[test]
3300 fn test_message_types() {
3301 let mut cache = NotificationCache::new();
3302
3303 cache.store_message(MessageType::Error, "error".to_string());
3304 cache.store_message(MessageType::Warning, "warning".to_string());
3305 cache.store_message(MessageType::Info, "info".to_string());
3306 cache.store_message(MessageType::Log, "log".to_string());
3307
3308 let messages = cache.messages();
3309 assert_eq!(messages[0].message_type, MessageType::Error);
3310 assert_eq!(messages[1].message_type, MessageType::Warning);
3311 assert_eq!(messages[2].message_type, MessageType::Info);
3312 assert_eq!(messages[3].message_type, MessageType::Log);
3313 }
3314
3315 #[test]
3316 fn test_timestamp_ordering() {
3317 let mut cache = NotificationCache::new();
3318
3319 cache.store_log(LogLevel::Info, "first".to_string());
3320 std::thread::sleep(std::time::Duration::from_millis(10));
3321 cache.store_log(LogLevel::Info, "second".to_string());
3322
3323 let logs = cache.logs();
3324 assert!(logs[0].timestamp < logs[1].timestamp);
3325 }
3326
3327 #[test]
3328 fn test_store_diagnostics_empty_list() {
3329 let mut cache = NotificationCache::new();
3330 let uri: Uri = Uri::from("file:///test.rs");
3331
3332 let diagnostic = LspDiagnostic {
3333 range: Range {
3334 start: Position {
3335 line: 0,
3336 character: 0,
3337 },
3338 end: Position {
3339 line: 0,
3340 character: 5,
3341 },
3342 },
3343 severity: Some(lsp_types::DiagnosticSeverity::Error),
3344 message: "test error".to_string().into(),
3345 code: None,
3346 source: None,
3347 code_description: None,
3348 related_information: None,
3349 tags: None,
3350 data: None,
3351 };
3352
3353 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
3354 assert_eq!(cache.diagnostics(&uri).unwrap().diagnostics.len(), 1);
3355
3356 cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
3357 let stored = cache.diagnostics(&uri).unwrap();
3358 assert_eq!(stored.diagnostics.len(), 0);
3359 assert_eq!(stored.version.map(DocumentVersion::get), Some(2));
3360 }
3361
3362 #[test]
3363 fn test_store_many_diagnostics_single_file() {
3364 let mut cache = NotificationCache::new();
3365 let uri: Uri = Uri::from("file:///test.rs");
3366
3367 let diagnostics: Vec<LspDiagnostic> = (0..100)
3368 .map(|i| LspDiagnostic {
3369 range: Range {
3370 start: Position {
3371 line: i,
3372 character: 0,
3373 },
3374 end: Position {
3375 line: i,
3376 character: 10,
3377 },
3378 },
3379 message: format!("Error {i}").into(),
3380 severity: Some(lsp_types::DiagnosticSeverity::Error),
3381 code: None,
3382 source: None,
3383 code_description: None,
3384 related_information: None,
3385 tags: None,
3386 data: None,
3387 })
3388 .collect();
3389
3390 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
3391
3392 let stored = cache.diagnostics(&uri).unwrap();
3393 assert_eq!(stored.diagnostics.len(), 100);
3394 }
3395
3396 #[test]
3397 fn test_logs_exact_capacity_boundary() {
3398 let mut cache = NotificationCache::new();
3399
3400 for i in 0..MAX_LOG_ENTRIES {
3401 cache.store_log(LogLevel::Info, format!("message {i}"));
3402 }
3403 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
3404
3405 cache.store_log(LogLevel::Info, "overflow".to_string());
3406 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
3407 assert_eq!(cache.logs().front().unwrap().message, "message 1");
3408 }
3409
3410 #[test]
3411 fn test_messages_exact_capacity_boundary() {
3412 let mut cache = NotificationCache::new();
3413
3414 for i in 0..MAX_SERVER_MESSAGES {
3415 cache.store_message(MessageType::Info, format!("message {i}"));
3416 }
3417 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
3418
3419 cache.store_message(MessageType::Info, "overflow".to_string());
3420 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
3421 assert_eq!(cache.messages().front().unwrap().message, "message 1");
3422 }
3423
3424 #[test]
3425 fn test_diagnostics_max_capacity() {
3426 let mut cache = NotificationCache::new();
3427
3428 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 10 {
3429 let uri: Uri = Uri::from(format!("file:///test{i}.rs"));
3430 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3431 }
3432
3433 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3434
3435 let evicted: Uri = Uri::from("file:///test0.rs");
3437 assert!(cache.diagnostics(&evicted).is_none());
3438 let newest: Uri = Uri::from(format!("file:///test{}.rs", MAX_DIAGNOSTIC_ENTRIES + 9));
3439 assert!(cache.diagnostics(&newest).is_some());
3440 }
3441
3442 #[test]
3443 fn test_diagnostics_replacing_existing_uri_does_not_trigger_eviction() {
3444 let mut cache = NotificationCache::new();
3445 let uri: Uri = Uri::from("file:///stable.rs");
3446
3447 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
3448 cache.store_diagnostics(
3449 &test_server(),
3450 &uri,
3451 Some(i32::try_from(i).unwrap()),
3452 vec![],
3453 );
3454 }
3455 assert_eq!(cache.diagnostics_count(), 1);
3456 assert!(cache.diagnostics(&uri).is_some());
3457 }
3458
3459 #[test]
3460 fn test_diagnostics_republish_refreshes_eviction_order() {
3461 let mut cache = NotificationCache::new();
3465 let actively_edited: Uri = Uri::from("file:///keep.rs");
3466 cache.store_diagnostics(&test_server(), &actively_edited, Some(1), vec![]);
3467
3468 for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
3470 let uri: Uri = Uri::from(format!("file:///untouched{i}.rs"));
3471 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3472 }
3473 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3474
3475 cache.store_diagnostics(&test_server(), &actively_edited, Some(2), vec![]);
3479
3480 let overflow: Uri = Uri::from("file:///overflow.rs");
3483 cache.store_diagnostics(&test_server(), &overflow, Some(1), vec![]);
3484
3485 assert!(
3486 cache.diagnostics(&actively_edited).is_some(),
3487 "republished entry must survive eviction after being refreshed"
3488 );
3489 let oldest_untouched: Uri = Uri::from("file:///untouched0.rs");
3490 assert!(
3491 cache.diagnostics(&oldest_untouched).is_none(),
3492 "the oldest never-republished entry must be evicted instead"
3493 );
3494 assert!(cache.diagnostics(&overflow).is_some());
3495 }
3496
3497 #[test]
3498 fn test_store_diagnostics_no_version() {
3499 let mut cache = NotificationCache::new();
3500 let uri: Uri = Uri::from("file:///test.rs");
3501
3502 cache.store_diagnostics(&test_server(), &uri, None, vec![]);
3503 let stored = cache.diagnostics(&uri).unwrap();
3504 assert_eq!(stored.version, None);
3505 }
3506
3507 #[test]
3513 fn test_noisy_server_does_not_evict_quiet_server_entries() {
3514 let mut cache = NotificationCache::new();
3515 cache.set_diagnostics_route_count(2);
3516 let noisy = ServerId::from_static("noisy");
3517 let quiet = ServerId::from_static("quiet");
3518
3519 let quiet_uri: Uri = Uri::from("file:///quiet/only_file.rs");
3520 cache.store_diagnostics(&quiet, &quiet_uri, Some(1), vec![]);
3521
3522 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 50 {
3527 let uri: Uri = Uri::from(format!("file:///noisy/file{i}.rs"));
3528 cache.store_diagnostics(&noisy, &uri, Some(1), vec![]);
3529 }
3530
3531 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3532 assert!(
3533 cache.diagnostics(&quiet_uri).is_some(),
3534 "quiet server's only entry must survive the noisy server's overflow"
3535 );
3536
3537 let noisy_first: Uri = Uri::from("file:///noisy/file0.rs");
3538 assert!(
3539 cache.diagnostics(&noisy_first).is_none(),
3540 "noisy server's own oldest entries must be evicted once the aggregate cache is full"
3541 );
3542 }
3543
3544 #[test]
3550 fn test_dominant_server_exceeds_equal_share_while_others_idle() {
3551 let mut cache = NotificationCache::new();
3552 cache.set_diagnostics_route_count(4);
3553 let dominant = ServerId::from_static("dominant");
3554
3555 let equal_share = MAX_DIAGNOSTIC_ENTRIES / 4;
3556 let more_than_share = equal_share + 100;
3557 for i in 0..more_than_share {
3558 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
3559 cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
3560 }
3561 assert_eq!(
3562 cache.diagnostics_count(),
3563 more_than_share,
3564 "a dominant server must be able to exceed its static equal share while the aggregate has room"
3565 );
3566
3567 for i in more_than_share..MAX_DIAGNOSTIC_ENTRIES {
3570 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
3571 cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
3572 }
3573 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3574 }
3575
3576 #[test]
3589 fn test_eviction_target_tie_break_is_deterministic() {
3590 let mut cache = NotificationCache::new();
3591 cache.set_diagnostics_route_count(1000); let a = ServerId::from_static("a");
3594 let b = ServerId::from_static("b");
3595 for i in 0..2 {
3596 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
3597 cache.store_diagnostics(&a, &uri, Some(1), vec![]);
3598 }
3599 for i in 0..2 {
3600 let uri: Uri = Uri::from(format!("file:///b/file{i}.rs"));
3601 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
3602 }
3603
3604 let writer = ServerId::from_static("writer");
3608 assert_eq!(cache.server_to_evict_from(&writer), Some(b));
3609 }
3610
3611 #[test]
3620 fn test_new_writer_still_evicts_when_every_existing_server_is_in_share() {
3621 let mut cache = NotificationCache::new();
3622 cache.set_diagnostics_route_count(2); let a = ServerId::from_static("a");
3625 let b = ServerId::from_static("b");
3626 for i in 0..500 {
3627 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
3628 cache.store_diagnostics(&a, &uri, Some(1), vec![]);
3629 }
3630 for i in 0..500 {
3631 let uri: Uri = Uri::from(format!("file:///b/file{i}.rs"));
3632 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
3633 }
3634 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3635
3636 let c = ServerId::from_static("c");
3639 let new_uri: Uri = Uri::from("file:///c/first.rs");
3640 cache.store_diagnostics(&c, &new_uri, Some(1), vec![]);
3641
3642 assert_eq!(
3643 cache.diagnostics_count(),
3644 MAX_DIAGNOSTIC_ENTRIES,
3645 "the aggregate cap must still be enforced even when every existing server is within share"
3646 );
3647 assert!(cache.diagnostics(&new_uri).is_some());
3648
3649 let b_oldest: Uri = Uri::from("file:///b/file0.rs");
3653 assert!(
3654 cache.diagnostics(&b_oldest).is_none(),
3655 "the largest in-share server (tie-broken to b) must lose its oldest entry"
3656 );
3657 assert!(
3658 cache
3659 .diagnostics(&Uri::from("file:///a/file0.rs".to_owned()))
3660 .is_some(),
3661 "the other in-share server must be untouched"
3662 );
3663 }
3664
3665 #[test]
3668 fn test_repeated_writes_same_owner_do_not_grow_order_map() {
3669 let mut cache = NotificationCache::new();
3670 let server = ServerId::from_static("server");
3671 let uri: Uri = Uri::from("file:///test.rs");
3672
3673 let max_version = i32::try_from(MAX_DIAGNOSTIC_ENTRIES).unwrap() + 10;
3674 for version in 0..max_version {
3675 cache.store_diagnostics(&server, &uri, Some(version), vec![]);
3676 }
3677
3678 assert_eq!(cache.diagnostics_count(), 1);
3679 let stored = cache.diagnostics(&uri).unwrap();
3680 assert_eq!(
3681 stored.version.map(DocumentVersion::get),
3682 Some(max_version - 1)
3683 );
3684 }
3685
3686 #[test]
3690 fn test_store_diagnostics_reassigns_ownership() {
3691 let mut cache = NotificationCache::new();
3692 let old_owner = ServerId::from_static("old");
3693 let new_owner = ServerId::from_static("new");
3694 let uri: Uri = Uri::from("file:///test.rs");
3695
3696 cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
3697 cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
3698
3699 assert_eq!(cache.diagnostics_count(), 1);
3700 let stored = cache.diagnostics(&uri).unwrap();
3701 assert_eq!(stored.version.map(DocumentVersion::get), Some(2));
3702
3703 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 5 {
3707 let other: Uri = Uri::from(format!("file:///old/file{i}.rs"));
3708 cache.store_diagnostics(&old_owner, &other, Some(1), vec![]);
3709 }
3710 assert!(cache.diagnostics(&uri).is_some());
3711 }
3712
3713 #[test]
3718 fn test_diagnostics_owner_returns_publisher_after_store() {
3719 let mut cache = NotificationCache::new();
3720 let server = ServerId::from_static("rust");
3721 let uri: Uri = Uri::from("file:///main.rs");
3722
3723 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
3724
3725 assert_eq!(cache.diagnostics_owner(&uri), Some(&server));
3726 }
3727
3728 #[test]
3729 fn test_diagnostics_owner_none_for_untracked_uri() {
3730 let cache = NotificationCache::new();
3731 let uri: Uri = Uri::from("file:///never-seen.rs");
3732
3733 assert_eq!(cache.diagnostics_owner(&uri), None);
3734 }
3735
3736 #[test]
3741 fn test_diagnostics_owner_reflects_reassigned_ownership() {
3742 let mut cache = NotificationCache::new();
3743 let old_owner = ServerId::from_static("old");
3744 let new_owner = ServerId::from_static("new");
3745 let uri: Uri = Uri::from("file:///test.rs");
3746
3747 cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
3748 assert_eq!(cache.diagnostics_owner(&uri), Some(&old_owner));
3749
3750 cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
3751 assert_eq!(cache.diagnostics_owner(&uri), Some(&new_owner));
3752 }
3753
3754 #[test]
3757 fn test_clear_server_diagnostics_scopes_to_one_server() {
3758 let mut cache = NotificationCache::new();
3759 let crashed = ServerId::from_static("crashed");
3760 let healthy = ServerId::from_static("healthy");
3761
3762 let crashed_uri: Uri = Uri::from("file:///crashed/main.py");
3763 let healthy_uri: Uri = Uri::from("file:///healthy/main.rs");
3764 cache.store_diagnostics(&crashed, &crashed_uri, Some(1), vec![]);
3765 cache.store_diagnostics(&healthy, &healthy_uri, Some(1), vec![]);
3766
3767 cache.clear_server_diagnostics(&crashed);
3768
3769 assert!(cache.diagnostics(&crashed_uri).is_none());
3770 assert!(cache.diagnostics(&healthy_uri).is_some());
3771 assert_eq!(cache.diagnostics_count(), 1);
3772
3773 cache.clear_server_diagnostics(&crashed);
3775 assert_eq!(cache.diagnostics_count(), 1);
3776 }
3777
3778 #[test]
3783 fn test_push_degraded_is_scoped_per_server_and_permanent() {
3784 let mut cache = NotificationCache::new();
3785 let degraded = ServerId::from_static("degraded");
3786 let healthy = ServerId::from_static("healthy");
3787
3788 assert!(!cache.is_push_degraded(°raded));
3789 assert!(!cache.is_push_degraded(&healthy));
3790
3791 cache.mark_push_degraded(°raded);
3792
3793 assert!(cache.is_push_degraded(°raded));
3794 assert!(!cache.is_push_degraded(&healthy));
3795
3796 cache.mark_push_degraded(°raded);
3798 assert!(cache.is_push_degraded(°raded));
3799 }
3800
3801 #[test]
3809 fn test_shrinking_budget_affects_eviction_target_not_existing_entries() {
3810 let mut cache = NotificationCache::new();
3811 let server = ServerId::from_static("server");
3812
3813 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
3814 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
3815 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
3816 }
3817 assert_eq!(
3818 cache.diagnostics_count(),
3819 MAX_DIAGNOSTIC_ENTRIES,
3820 "filling to the aggregate cap must not evict anything early"
3821 );
3822
3823 cache.set_diagnostics_route_count(4);
3826 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3827
3828 let other = ServerId::from_static("other");
3831 let new_uri: Uri = Uri::from("file:///other/new.rs");
3832 cache.store_diagnostics(&other, &new_uri, Some(1), vec![]);
3833
3834 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3835 assert!(cache.diagnostics(&new_uri).is_some());
3836 let server_oldest: Uri = Uri::from("file:///file0.rs");
3837 assert!(
3838 cache.diagnostics(&server_oldest).is_none(),
3839 "the pre-existing server's oldest entry, now far over its shrunk share, must be evicted"
3840 );
3841 }
3842
3843 #[test]
3849 fn test_fair_share_applies_by_default_without_explicit_route_count() {
3850 let mut cache = NotificationCache::new();
3851 let noisy = ServerId::from_static("noisy");
3852 let quiet = ServerId::from_static("quiet");
3853
3854 let quiet_uri: Uri = Uri::from("file:///quiet/only_file.rs");
3855 cache.store_diagnostics(&quiet, &quiet_uri, Some(1), vec![]);
3856
3857 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 50 {
3859 let uri: Uri = Uri::from(format!("file:///noisy/file{i}.rs"));
3860 cache.store_diagnostics(&noisy, &uri, Some(1), vec![]);
3861 }
3862
3863 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3864 assert!(
3865 cache.diagnostics(&quiet_uri).is_some(),
3866 "quiet server's only entry must survive even without ever calling \
3867 set_diagnostics_route_count"
3868 );
3869 let noisy_first: Uri = Uri::from("file:///noisy/file0.rs");
3870 assert!(
3871 cache.diagnostics(&noisy_first).is_none(),
3872 "the noisy server, now auto-derived as one of two servers sharing the budget, \
3873 must still lose its own oldest entries once over its fair share"
3874 );
3875 }
3876
3877 #[test]
3883 fn test_single_server_gets_full_budget_without_explicit_route_count() {
3884 let mut cache = NotificationCache::new();
3885
3886 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
3887 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
3888 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3889 }
3890
3891 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3892 }
3893
3894 #[test]
3900 fn test_empty_diagnostics_entries_evicted_before_non_empty_ones() {
3901 let mut cache = NotificationCache::new();
3902 let server = test_server();
3903
3904 let important: Uri = Uri::from("file:///important.rs");
3905 cache.store_diagnostics(
3906 &server,
3907 &important,
3908 Some(1),
3909 vec![minimal_diagnostic("real error".to_string())],
3910 );
3911
3912 for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
3916 let uri: Uri = Uri::from(format!("file:///clean{i}.rs"));
3917 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
3918 }
3919 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3920
3921 let overflow: Uri = Uri::from("file:///overflow.rs");
3925 cache.store_diagnostics(&server, &overflow, Some(1), vec![]);
3926
3927 assert!(
3928 cache.diagnostics(&important).is_some(),
3929 "a non-empty entry must survive eviction over empty entries, even though it is older"
3930 );
3931 let oldest_clean: Uri = Uri::from("file:///clean0.rs");
3932 assert!(
3933 cache.diagnostics(&oldest_clean).is_none(),
3934 "the oldest empty entry must be evicted instead of the older non-empty one"
3935 );
3936 assert!(cache.diagnostics(&overflow).is_some());
3937 }
3938
3939 #[test]
3947 fn test_empty_diagnostics_entry_is_still_tracked_until_evicted() {
3948 let mut cache = NotificationCache::new();
3949 let uri: Uri = Uri::from("file:///clean.rs");
3950
3951 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3952
3953 let stored = cache.diagnostics(&uri);
3954 assert!(
3955 stored.is_some(),
3956 "an empty-diagnostics entry must still be tracked"
3957 );
3958 assert_eq!(stored.unwrap().diagnostics.len(), 0);
3959 }
3960
3961 #[test]
3966 fn test_over_share_servers_empty_entry_evicted_before_a_different_servers_real_diagnostic() {
3967 let mut cache = NotificationCache::new();
3968 cache.set_diagnostics_route_count(3); let a = ServerId::from_static("a"); let b = ServerId::from_static("b"); let c = ServerId::from_static("c"); for i in 0..500 {
3975 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
3976 cache.store_diagnostics(
3977 &a,
3978 &uri,
3979 Some(1),
3980 vec![minimal_diagnostic(format!("error {i}"))],
3981 );
3982 }
3983 for i in 0..400 {
3984 let uri: Uri = Uri::from(format!("file:///b/file{i}.rs"));
3985 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
3986 }
3987 for i in 0..100 {
3988 let uri: Uri = Uri::from(format!("file:///c/file{i}.rs"));
3989 cache.store_diagnostics(&c, &uri, Some(1), vec![]);
3990 }
3991 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3992
3993 let overflow: Uri = Uri::from("file:///a/overflow.rs");
3998 cache.store_diagnostics(
3999 &a,
4000 &overflow,
4001 Some(1),
4002 vec![minimal_diagnostic("overflow error".to_string())],
4003 );
4004
4005 for i in 0..500 {
4006 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
4007 assert!(
4008 cache.diagnostics(&uri).is_some(),
4009 "server a's real diagnostics must all survive; b has an empty entry to lose \
4010 instead"
4011 );
4012 }
4013 let b_oldest: Uri = Uri::from("file:///b/file0.rs");
4014 assert!(
4015 cache.diagnostics(&b_oldest).is_none(),
4016 "b's oldest empty entry must be evicted instead of a's real diagnostics"
4017 );
4018 assert!(cache.diagnostics(&overflow).is_some());
4019 }
4020
4021 #[test]
4026 fn test_dirty_then_clean_then_dirty_again_updates_emptiness_tracking() {
4027 let mut cache = NotificationCache::new();
4028 let server = test_server();
4029 let uri: Uri = Uri::from("file:///flapping.rs");
4030
4031 cache.store_diagnostics(
4032 &server,
4033 &uri,
4034 Some(1),
4035 vec![minimal_diagnostic("first error".to_string())],
4036 );
4037 cache.store_diagnostics(&server, &uri, Some(2), vec![]); cache.store_diagnostics(
4039 &server,
4040 &uri,
4041 Some(3),
4042 vec![minimal_diagnostic("second error".to_string())],
4043 ); for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
4049 let other: Uri = Uri::from(format!("file:///clean{i}.rs"));
4050 cache.store_diagnostics(&server, &other, Some(1), vec![]);
4051 }
4052 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
4053
4054 let overflow: Uri = Uri::from("file:///overflow.rs");
4055 cache.store_diagnostics(&server, &overflow, Some(1), vec![]);
4056
4057 let stored = cache.diagnostics(&uri);
4058 assert!(
4059 stored.is_some_and(|info| info.diagnostics.len() == 1),
4060 "the re-dirtied entry must survive and keep its real diagnostic, not be mistaken \
4061 for an empty entry"
4062 );
4063 }
4064
4065 #[test]
4068 fn test_set_diagnostics_route_count_zero_clamps_to_one() {
4069 let mut cache = NotificationCache::new();
4070 cache.set_diagnostics_route_count(0);
4071
4072 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 5 {
4073 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
4074 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
4075 }
4076
4077 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
4078 }
4079
4080 #[test]
4083 fn test_indexing_state_defaults_unknown() {
4084 let cache = NotificationCache::new();
4085 assert_eq!(cache.indexing_state(&test_server()), IndexingState::Unknown);
4086 }
4087
4088 #[test]
4089 fn test_observe_indexing_signal_delegates_to_tracker() {
4090 let mut cache = NotificationCache::new();
4091 let server = test_server();
4092 cache.observe_indexing_signal(
4093 &server,
4094 "experimental/serverStatus",
4095 Some(&serde_json::json!({"quiescent": false})),
4096 );
4097 assert_eq!(cache.indexing_state(&server), IndexingState::Loading);
4098 }
4099
4100 #[test]
4101 fn test_reset_indexing_state_delegates_to_tracker() {
4102 let mut cache = NotificationCache::new();
4103 let server = test_server();
4104 cache.observe_indexing_signal(
4105 &server,
4106 "experimental/serverStatus",
4107 Some(&serde_json::json!({"quiescent": false})),
4108 );
4109 cache.reset_indexing_state(&server, IndexingReset::Forget);
4110 assert_eq!(cache.indexing_state(&server), IndexingState::Unknown);
4111 }
4112
4113 fn file_uri(path: &str) -> Uri {
4114 Uri::from(format!("file:///ws/{path}"))
4115 }
4116
4117 fn published(source: &str) -> PublishedDiagnosticsUri {
4118 PublishedDiagnosticsUri::for_test(file_uri(source), file_uri("main.rs"))
4119 }
4120
4121 fn diagnostic_at(line: u32, message: &str) -> LspDiagnostic {
4122 let mut diagnostic = minimal_diagnostic(message.to_owned());
4123 diagnostic.range.start.line = line;
4124 diagnostic.range.end.line = line;
4125 diagnostic
4126 }
4127
4128 fn merged(cache: &NotificationCache) -> Option<DiagnosticInfo> {
4129 cache.diagnostic_sources(&file_uri("main.rs")).merge()
4130 }
4131
4132 fn messages(info: &DiagnosticInfo) -> Vec<String> {
4133 info.diagnostics
4134 .iter()
4135 .map(|d| message_as_str(&d.message).to_owned())
4136 .collect()
4137 }
4138
4139 #[test]
4142 fn test_published_alias_errors_survive_empty_canonical_publish() {
4143 let mut cache = NotificationCache::new();
4144 let server = test_server();
4145 let errors = vec![diagnostic_at(1, "e1"), diagnostic_at(2, "e2")];
4146
4147 cache.store_published_diagnostics(&server, &published("alias.rs"), None, errors);
4148 cache.store_published_diagnostics(&server, &published("main.rs"), Some(3), vec![]);
4149 assert_eq!(messages(&merged(&cache).unwrap()), ["e1", "e2"]);
4150
4151 cache.store_published_diagnostics(&server, &published("main.rs"), Some(4), vec![]);
4152 assert_eq!(messages(&merged(&cache).unwrap()), ["e1", "e2"]);
4153
4154 cache.store_published_diagnostics(&server, &published("alias.rs"), None, vec![]);
4155 assert_eq!(merged(&cache).unwrap().diagnostics.len(), 0);
4156 }
4157
4158 #[test]
4159 fn test_published_empty_publish_clears_only_its_own_source() {
4160 let mut cache = NotificationCache::new();
4161 let server = test_server();
4162 cache.store_published_diagnostics(
4163 &server,
4164 &published("a1.rs"),
4165 None,
4166 vec![diagnostic_at(1, "e1")],
4167 );
4168 cache.store_published_diagnostics(
4169 &server,
4170 &published("a2.rs"),
4171 None,
4172 vec![diagnostic_at(2, "e2")],
4173 );
4174 assert_eq!(messages(&merged(&cache).unwrap()), ["e1", "e2"]);
4175
4176 cache.store_published_diagnostics(&server, &published("a1.rs"), None, vec![]);
4177
4178 assert_eq!(messages(&merged(&cache).unwrap()), ["e2"]);
4179 }
4180
4181 #[test]
4183 fn test_merge_removes_duplicates_sharing_a_start_with_another_diagnostic() {
4184 let mut cache = NotificationCache::new();
4185 let server = test_server();
4186 let x = diagnostic_at(1, "x");
4187 let y = diagnostic_at(1, "y");
4188 cache.store_published_diagnostics(&server, &published("a1.rs"), None, vec![x.clone(), y]);
4189 cache.store_published_diagnostics(&server, &published("a2.rs"), None, vec![x]);
4190
4191 assert_eq!(messages(&merged(&cache).unwrap()), ["x", "y"]);
4192 }
4193
4194 #[test]
4195 fn test_merge_orders_by_range_start() {
4196 let mut cache = NotificationCache::new();
4197 let server = test_server();
4198 cache.store_published_diagnostics(
4199 &server,
4200 &published("a1.rs"),
4201 None,
4202 vec![diagnostic_at(5, "late")],
4203 );
4204 cache.store_published_diagnostics(
4205 &server,
4206 &published("a2.rs"),
4207 None,
4208 vec![diagnostic_at(1, "early")],
4209 );
4210
4211 assert_eq!(messages(&merged(&cache).unwrap()), ["early", "late"]);
4212 }
4213
4214 #[test]
4215 fn test_merged_version_is_the_canonical_sources() {
4216 let mut cache = NotificationCache::new();
4217 let server = test_server();
4218 cache.store_published_diagnostics(
4219 &server,
4220 &published("main.rs"),
4221 Some(7),
4222 vec![diagnostic_at(1, "a")],
4223 );
4224 cache.store_published_diagnostics(
4225 &server,
4226 &published("alias.rs"),
4227 None,
4228 vec![diagnostic_at(2, "b")],
4229 );
4230 assert_eq!(
4231 merged(&cache).unwrap().version.map(DocumentVersion::get),
4232 Some(7)
4233 );
4234
4235 cache.store_published_diagnostics(
4236 &server,
4237 &published("main.rs"),
4238 Some(8),
4239 vec![diagnostic_at(1, "a")],
4240 );
4241 assert_eq!(
4242 merged(&cache).unwrap().version.map(DocumentVersion::get),
4243 Some(8)
4244 );
4245 }
4246
4247 #[test]
4248 fn test_merge_is_bounded_by_the_entry_size_cap() {
4249 let mut cache = NotificationCache::new();
4250 let server = test_server();
4251 let big = "x".repeat(200 * 1024);
4252 for source in [
4253 "a1.rs", "a2.rs", "a3.rs", "a4.rs", "a5.rs", "a6.rs", "a7.rs", "a8.rs",
4254 ] {
4255 let diagnostics = (0..4)
4256 .map(|n| diagnostic_at(n, &format!("{source}-{n}-{big}")))
4257 .collect();
4258 cache.store_published_diagnostics(&server, &published(source), None, diagnostics);
4259 }
4260
4261 let info = merged(&cache).unwrap();
4262
4263 let bytes = serde_json::to_vec(&info.diagnostics).unwrap().len();
4264 assert!(bytes <= MAX_DIAGNOSTICS_ENTRY_BYTES, "{bytes} bytes");
4265 }
4266
4267 #[test]
4268 fn test_single_source_is_returned_unchanged() {
4269 let mut cache = NotificationCache::new();
4270 cache.store_published_diagnostics(
4271 &test_server(),
4272 &published("alias.rs"),
4273 Some(2),
4274 vec![diagnostic_at(1, "e")],
4275 );
4276
4277 let info = merged(&cache).unwrap();
4278
4279 assert_eq!(info.uri, file_uri("alias.rs"));
4280 assert_eq!(info.version.map(DocumentVersion::get), Some(2));
4281 }
4282
4283 #[test]
4284 fn test_ninth_source_is_dropped_without_an_orphan_entry() {
4285 let mut cache = NotificationCache::new();
4286 let server = test_server();
4287 for n in 0..MAX_SOURCES_PER_FILE {
4288 cache.store_published_diagnostics(
4289 &server,
4290 &published(&format!("a{n}.rs")),
4291 None,
4292 vec![diagnostic_at(1, "e")],
4293 );
4294 }
4295
4296 cache.store_published_diagnostics(
4297 &server,
4298 &published("extra.rs"),
4299 None,
4300 vec![diagnostic_at(9, "extra")],
4301 );
4302
4303 assert_eq!(cache.diagnostics_count(), MAX_SOURCES_PER_FILE);
4304 assert!(cache.diagnostics(&file_uri("extra.rs")).is_none());
4305 assert!(!messages(&merged(&cache).unwrap()).contains(&"extra".to_owned()));
4306 }
4307
4308 #[test]
4311 fn test_lost_publish_replaces_older_entry_with_a_lost_mark() {
4312 let mut cache = NotificationCache::new();
4313 let server = test_server();
4314 let uri = file_uri("a.rs");
4315 cache.store_diagnostics(&server, &uri, None, vec![diagnostic_at(1, "old")]);
4316
4317 let lost = [PublishedDiagnosticsUri::for_test(uri.clone(), uri.clone())];
4318 let changed = cache.record_lost_files(&server, &lost);
4319
4320 assert_eq!(changed, [DiagnosticsKey::of(&uri)]);
4321 assert!(!cache.has_diagnostics(&uri));
4322 assert_eq!(
4323 cache.availability(&uri, Some(&server)),
4324 DiagnosticsAvailability::Evicted
4325 );
4326 cache.store_diagnostics(&server, &uri, None, vec![diagnostic_at(2, "new")]);
4327 assert_eq!(
4328 cache.availability(&uri, Some(&server)),
4329 DiagnosticsAvailability::Published
4330 );
4331 cache.assert_consistent();
4332 }
4333
4334 #[test]
4336 fn test_lost_publish_batch_reports_each_changed_file_once_in_key_order() {
4337 let mut cache = NotificationCache::new();
4338 let server = test_server();
4339 let lost = |name: &str| {
4340 let uri = file_uri(name);
4341 PublishedDiagnosticsUri::for_test(uri.clone(), uri)
4342 };
4343 let batch: Vec<_> = (0..1000)
4344 .rev()
4345 .flat_map(|n| {
4346 let name = format!("f{n:04}.rs");
4347 [lost(&name), lost(&name)]
4348 })
4349 .collect();
4350
4351 let changed = cache.record_lost_files(&server, &batch);
4352
4353 let expected: Vec<_> = (0..1000)
4354 .map(|n| DiagnosticsKey::of(&file_uri(&format!("f{n:04}.rs"))))
4355 .collect();
4356 assert_eq!(changed, expected);
4357 }
4358
4359 #[test]
4362 fn test_lost_publish_reports_only_files_whose_view_changed() {
4363 let mut cache = NotificationCache::new();
4364 let server = test_server();
4365 let other = ServerId::from_static("other");
4366 let uri = file_uri("a.rs");
4367 let shared = file_uri("b.rs");
4368 let lost = |uri: &Uri| PublishedDiagnosticsUri::for_test(uri.clone(), uri.clone());
4369 cache.store_diagnostics(&other, &shared, None, vec![diagnostic_at(1, "theirs")]);
4370
4371 let first = cache.record_lost_files(&server, &[lost(&uri), lost(&shared)]);
4372 let second = cache.record_lost_files(&server, &[lost(&uri), lost(&shared)]);
4373
4374 assert_eq!(first, [DiagnosticsKey::of(&uri)]);
4375 assert!(second.is_empty(), "{second:?}");
4376 assert!(cache.has_diagnostics(&shared));
4377 }
4378
4379 #[test]
4382 fn test_fallback_lost_publish_evicts_the_symlinked_entry() {
4383 let mut cache = NotificationCache::new();
4384 let server = test_server();
4385 let link = file_uri("link.rs");
4386 let real = file_uri("main.rs");
4387 cache.store_published_diagnostics(
4388 &server,
4389 &PublishedDiagnosticsUri::for_test(link.clone(), real.clone()),
4390 None,
4391 vec![diagnostic_at(1, "old")],
4392 );
4393 assert!(cache.has_diagnostics(&real));
4394
4395 let fallback = PublishedDiagnosticsUri::for_test_with_form(
4396 link.clone(),
4397 link,
4398 CanonicalForm::Fallback,
4399 );
4400 let changed = cache.record_lost_files(&server, &[fallback]);
4401
4402 assert_eq!(changed, [DiagnosticsKey::of(&real)]);
4403 assert!(!cache.has_diagnostics(&real));
4404 cache.assert_consistent();
4405 }
4406
4407 #[test]
4409 fn test_unnamed_lost_publishes_make_unknown_files_read_evicted() {
4410 let mut cache = NotificationCache::new();
4411 let server = test_server();
4412
4413 cache.record_unnamed_loss(&server);
4414
4415 assert_eq!(
4416 cache.availability(&file_uri("unknown.rs"), Some(&server)),
4417 DiagnosticsAvailability::Evicted
4418 );
4419 }
4420
4421 #[test]
4423 fn test_a_publish_over_the_source_cap_is_reported_as_dropped() {
4424 let mut cache = NotificationCache::new();
4425 let server = test_server();
4426 for n in 0..MAX_SOURCES_PER_FILE {
4427 let write = cache.write_published_diagnostics(
4428 &server,
4429 &published(&format!("a{n}.rs")),
4430 None,
4431 BoundedDiagnostics::new(&file_uri("a.rs"), vec![diagnostic_at(1, "e")]),
4432 );
4433 std::assert_matches!(write, PushWrite::Stored { .. });
4434 }
4435
4436 let write = cache.write_published_diagnostics(
4437 &server,
4438 &published("extra.rs"),
4439 None,
4440 BoundedDiagnostics::new(&file_uri("a.rs"), vec![diagnostic_at(9, "extra")]),
4441 );
4442
4443 std::assert_matches!(write, PushWrite::Dropped { .. });
4444 }
4445
4446 #[test]
4447 fn test_republishing_an_indexed_source_at_the_cap_is_accepted() {
4448 let mut cache = NotificationCache::new();
4449 let server = test_server();
4450 for n in 0..MAX_SOURCES_PER_FILE {
4451 cache.store_published_diagnostics(
4452 &server,
4453 &published(&format!("a{n}.rs")),
4454 None,
4455 vec![],
4456 );
4457 }
4458
4459 cache.store_published_diagnostics(
4460 &server,
4461 &published("a0.rs"),
4462 None,
4463 vec![diagnostic_at(1, "again")],
4464 );
4465
4466 assert_eq!(messages(&merged(&cache).unwrap()), ["again"]);
4467 }
4468
4469 #[test]
4470 fn test_clear_server_diagnostics_prunes_the_source_index() {
4471 let mut cache = NotificationCache::new();
4472 let server = test_server();
4473 cache.store_published_diagnostics(
4474 &server,
4475 &published("a1.rs"),
4476 None,
4477 vec![diagnostic_at(1, "e")],
4478 );
4479
4480 cache.clear_server_diagnostics(&server);
4481
4482 cache.assert_consistent();
4483 assert!(cache.files.is_empty());
4484 assert!(!cache.has_diagnostics(&file_uri("main.rs")));
4485 assert!(merged(&cache).is_none());
4486 }
4487
4488 #[test]
4489 fn test_eviction_prunes_the_source_index() {
4490 let mut cache = NotificationCache::new();
4491 let server = test_server();
4492 cache.store_published_diagnostics(&server, &published("a1.rs"), None, vec![]);
4493 for n in 0..MAX_DIAGNOSTIC_ENTRIES {
4494 let uri = Uri::from(format!("file:///other/{n}.rs"));
4495 cache.store_diagnostics(&server, &uri, None, vec![]);
4496 }
4497
4498 assert!(cache.diagnostics(&file_uri("a1.rs")).is_none());
4499 cache.assert_consistent();
4500 assert!(
4501 !cache
4502 .files
4503 .contains_key(&DiagnosticsKey::of(&file_uri("main.rs")))
4504 );
4505 }
4506
4507 #[test]
4508 fn test_diagnostics_owner_resolves_through_the_source_index() {
4509 let mut cache = NotificationCache::new();
4510 let server = test_server();
4511 cache.store_published_diagnostics(&server, &published("alias.rs"), None, vec![]);
4512
4513 assert_eq!(cache.diagnostics_owner(&file_uri("main.rs")), Some(&server));
4514 }
4515
4516 #[test]
4519 fn test_canonical_source_is_admitted_when_aliases_fill_the_cap() {
4520 let mut cache = NotificationCache::new();
4521 let server = test_server();
4522 for n in 0..MAX_SOURCES_PER_FILE {
4523 cache.store_published_diagnostics(
4524 &server,
4525 &published(&format!("a{n}.rs")),
4526 None,
4527 vec![diagnostic_at(
4528 u32::try_from(n).unwrap(),
4529 &format!("alias{n}"),
4530 )],
4531 );
4532 }
4533
4534 cache.store_published_diagnostics(
4535 &server,
4536 &published("main.rs"),
4537 Some(5),
4538 vec![diagnostic_at(99, "canonical")],
4539 );
4540
4541 let info = merged(&cache).unwrap();
4542 let shown = messages(&info);
4543 assert!(shown.contains(&"canonical".to_owned()), "{shown:?}");
4544 assert!(
4545 !shown.contains(&"alias0".to_owned()),
4546 "oldest alias is evicted"
4547 );
4548 assert_eq!(shown.len(), MAX_SOURCES_PER_FILE);
4549 assert_eq!(info.version.map(DocumentVersion::get), Some(5));
4550 assert_eq!(cache.diagnostics_count(), MAX_SOURCES_PER_FILE);
4551 }
4552
4553 #[test]
4554 fn test_alias_beyond_the_cap_is_still_dropped_when_canonical_is_present() {
4555 let mut cache = NotificationCache::new();
4556 let server = test_server();
4557 cache.store_published_diagnostics(&server, &published("main.rs"), None, vec![]);
4558 for n in 0..MAX_SOURCES_PER_FILE - 1 {
4559 cache.store_published_diagnostics(
4560 &server,
4561 &published(&format!("a{n}.rs")),
4562 None,
4563 vec![],
4564 );
4565 }
4566
4567 cache.store_published_diagnostics(
4568 &server,
4569 &published("extra.rs"),
4570 None,
4571 vec![diagnostic_at(1, "x")],
4572 );
4573
4574 assert!(cache.diagnostics(&file_uri("extra.rs")).is_none());
4575 assert!(cache.diagnostics(&file_uri("main.rs")).is_some());
4576 }
4577
4578 #[test]
4581 fn test_merge_handles_many_same_start_diagnostics_across_sources() {
4582 const PER_SOURCE: u32 = 3000;
4583 let mut cache = NotificationCache::new();
4584 let server = test_server();
4585 for source in 0..MAX_SOURCES_PER_FILE {
4586 let diagnostics = (0..PER_SOURCE)
4587 .map(|n| diagnostic_at(0, &format!("diagnostic-{n}")))
4588 .collect();
4589 cache.store_published_diagnostics(
4590 &server,
4591 &published(&format!("a{source}.rs")),
4592 None,
4593 diagnostics,
4594 );
4595 }
4596
4597 let info = merged(&cache).unwrap();
4598
4599 assert_eq!(info.diagnostics.len(), PER_SOURCE as usize);
4600 }
4601
4602 #[test]
4605 fn test_merge_keeps_diagnostics_that_differ_outside_range_and_message() {
4606 let mut cache = NotificationCache::new();
4607 let server = test_server();
4608 let plain = diagnostic_at(1, "same");
4609 let mut coded = diagnostic_at(1, "same");
4610 coded.code = Some(lsp_types::Code::String("E1".to_owned()));
4611 cache.store_published_diagnostics(&server, &published("a1.rs"), None, vec![plain]);
4612 cache.store_published_diagnostics(&server, &published("a2.rs"), None, vec![coded]);
4613
4614 assert_eq!(merged(&cache).unwrap().diagnostics.len(), 2);
4615 }
4616
4617 #[test]
4618 fn test_clear_server_diagnostics_leaves_no_dangling_file_index() {
4619 let mut cache = NotificationCache::new();
4620 let (a, b) = (ServerId::from_static("a"), ServerId::from_static("b"));
4621 cache.store_published_diagnostics(&a, &published("a1.rs"), None, vec![]);
4622 cache.store_published_diagnostics(
4623 &b,
4624 &published("main.rs"),
4625 None,
4626 vec![diagnostic_at(1, "e")],
4627 );
4628
4629 cache.clear_server_diagnostics(&a);
4630
4631 cache.assert_consistent();
4632 assert_eq!(cache.files.len(), 1);
4633 assert!(cache.diagnostics(&file_uri("main.rs")).is_some());
4634 assert!(cache.diagnostics(&file_uri("a1.rs")).is_none());
4635 }
4636
4637 #[derive(Debug, Clone)]
4638 enum CacheOp {
4639 Publish {
4640 server: u8,
4641 file: u8,
4642 alias: Option<u8>,
4643 empty: bool,
4644 },
4645 Clear {
4646 server: u8,
4647 },
4648 }
4649
4650 fn cache_op() -> impl proptest::strategy::Strategy<Value = CacheOp> {
4651 use proptest::prelude::*;
4652
4653 prop_oneof![
4654 4 => (0..3u8, 0..3u8, proptest::option::of(0..3u8), any::<bool>()).prop_map(
4655 |(server, file, alias, empty)| CacheOp::Publish { server, file, alias, empty }
4656 ),
4657 1 => (0..3u8).prop_map(|server| CacheOp::Clear { server }),
4658 ]
4659 }
4660
4661 proptest::proptest! {
4662 #[test]
4663 fn cache_indices_stay_consistent_under_random_operations(
4664 ops in proptest::collection::vec(cache_op(), 0..60)
4665 ) {
4666 let mut cache = NotificationCache::new();
4667 for op in ops {
4668 match op {
4669 CacheOp::Publish { server, file, alias, empty } => {
4670 let canonical = file_uri(&format!("f{file}.rs"));
4671 let source = alias.map_or_else(
4672 || canonical.clone(),
4673 |n| file_uri(&format!("alias{n}.rs")),
4674 );
4675 let diagnostics = if empty { vec![] } else { vec![diagnostic_at(1, "e")] };
4676 cache.store_published_diagnostics(
4677 &ServerId::new(format!("s{server}")).unwrap(),
4678 &PublishedDiagnosticsUri::for_test(source, canonical),
4679 None,
4680 diagnostics,
4681 );
4682 }
4683 CacheOp::Clear { server } => {
4684 cache.clear_server_diagnostics(&ServerId::new(format!("s{server}")).unwrap());
4685 }
4686 }
4687 cache.assert_consistent();
4688 }
4689 }
4690 }
4691
4692 fn first_uri() -> Uri {
4693 Uri::from("file:///first.rs")
4694 }
4695
4696 fn error_diagnostic() -> LspDiagnostic {
4697 LspDiagnostic {
4698 severity: Some(lsp_types::DiagnosticSeverity::Error),
4699 message: "broken".to_owned().into(),
4700 ..LspDiagnostic::default()
4701 }
4702 }
4703
4704 fn full_cache(first_empty: bool) -> NotificationCache {
4707 let mut cache = NotificationCache::new();
4708 let first: Uri = Uri::from("file:///first.rs");
4709 let diagnostics = if first_empty {
4710 vec![]
4711 } else {
4712 vec![error_diagnostic()]
4713 };
4714 cache.store_diagnostics(&test_server(), &first, None, diagnostics);
4715 for i in 1..MAX_DIAGNOSTIC_ENTRIES {
4716 let uri: Uri = Uri::from(format!("file:///filler{i}.rs"));
4717 cache.store_diagnostics(&test_server(), &uri, None, vec![error_diagnostic()]);
4718 }
4719 cache
4720 }
4721
4722 fn evict_one(cache: &mut NotificationCache) {
4723 let overflow: Uri = Uri::from("file:///overflow.rs");
4724 cache.store_diagnostics(&test_server(), &overflow, None, vec![error_diagnostic()]);
4725 }
4726
4727 #[test]
4728 fn test_evicted_empty_entry_is_listen_replayable() {
4729 let mut cache = full_cache(true);
4730 assert!(cache.is_listen_replayable(&first_uri()));
4731 evict_one(&mut cache);
4732 assert!(!cache.has_diagnostics(&first_uri()));
4733 assert!(cache.is_listen_replayable(&first_uri()));
4734 assert!(!cache.is_listen_replayable(&Uri::from("file:///unrelated.rs")));
4735 }
4736
4737 fn write_error(cache: &mut NotificationCache, file: &str) -> PushWrite {
4738 let uri = Uri::from(file);
4739 cache.write_published_diagnostics(
4740 &test_server(),
4741 &PublishedDiagnosticsUri::for_test(uri.clone(), uri),
4742 None,
4743 BoundedDiagnostics::new(&Uri::from(file), vec![error_diagnostic()]),
4744 )
4745 }
4746
4747 #[test]
4749 fn test_a_push_reports_the_file_it_evicts() {
4750 let mut cache = full_cache(false);
4751 let write = write_error(&mut cache, "file:///overflow.rs");
4752 assert_eq!(write.evicted(), [DiagnosticsKey::of(&first_uri())]);
4753 assert!(!cache.has_diagnostics(&first_uri()));
4754 }
4755
4756 #[test]
4758 fn test_a_push_that_needs_no_room_reports_no_eviction() {
4759 let mut cache = full_cache(false);
4760 assert_eq!(
4761 write_error(&mut cache, "file:///first.rs").evicted().len(),
4762 0
4763 );
4764 let mut roomy = NotificationCache::new();
4765 assert_eq!(write_error(&mut roomy, "file:///a.rs").evicted().len(), 0);
4766 }
4767
4768 fn route() -> ServerId {
4769 test_server()
4770 }
4771
4772 #[test]
4775 fn test_evicted_clean_entry_reads_as_published() {
4776 let mut cache = full_cache(true);
4777 assert!(cache.has_diagnostics(&first_uri()));
4778 evict_one(&mut cache);
4779 assert!(!cache.has_diagnostics(&first_uri()));
4780 assert_eq!(
4781 cache.availability(&first_uri(), Some(&route())),
4782 DiagnosticsAvailability::Published
4783 );
4784 }
4785
4786 #[test]
4789 fn test_evicted_non_empty_entry_reads_as_evicted() {
4790 let mut cache = full_cache(false);
4791 evict_one(&mut cache);
4792 assert_eq!(
4793 cache.availability(&first_uri(), Some(&route())),
4794 DiagnosticsAvailability::Evicted
4795 );
4796 }
4797
4798 #[test]
4799 fn test_unpublished_file_reads_as_pending_even_next_to_evictions() {
4800 let mut cache = full_cache(false);
4801 evict_one(&mut cache);
4802 let unseen = Uri::from("file:///never-published.rs");
4803 assert_eq!(
4804 cache.availability(&unseen, Some(&route())),
4805 DiagnosticsAvailability::Pending
4806 );
4807 assert_eq!(
4808 cache.availability(&unseen, None),
4809 DiagnosticsAvailability::Pending
4810 );
4811 }
4812
4813 #[test]
4816 fn test_clearing_a_server_forgets_its_eviction_marks() {
4817 let mut cache = full_cache(false);
4818 evict_one(&mut cache);
4819 drop(cache.clear_server_diagnostics(&route()));
4820 assert_eq!(
4821 cache.availability(&first_uri(), Some(&route())),
4822 DiagnosticsAvailability::Pending
4823 );
4824 }
4825
4826 #[test]
4827 fn test_a_new_publish_replaces_the_mark_of_its_file() {
4828 let mut cache = full_cache(true);
4829 evict_one(&mut cache);
4830 let first: Uri = Uri::from("file:///first.rs");
4831 cache.store_diagnostics(&test_server(), &first, None, vec![error_diagnostic()]);
4832 assert_eq!(
4833 cache.availability(&first_uri(), None),
4834 DiagnosticsAvailability::Published
4835 );
4836 assert!(
4837 cache
4838 .eviction_marks
4839 .content(&DiagnosticsKey::of(&first))
4840 .is_none()
4841 );
4842 }
4843
4844 #[test]
4847 fn test_lost_content_survives_a_later_clean_eviction_of_the_file() {
4848 let mut marks = EvictionMarks::default();
4849 let file = DiagnosticsKey::of(&first_uri());
4850 marks.record(file.clone(), route(), EvictedContent::Lost);
4851 marks.record(file.clone(), route(), EvictedContent::Clean);
4852 assert_eq!(marks.content(&file), Some(EvictedContent::Lost));
4853 }
4854
4855 fn overflowed_after_churn() -> NotificationCache {
4856 let mut cache = full_cache(false);
4857 for i in 0..=MAX_EVICTION_MARKS {
4858 drop(write_error(&mut cache, &format!("file:///churn{i}.rs")));
4859 }
4860 assert!(cache.eviction_marks.has_overflowed(&route()));
4861 cache
4862 }
4863
4864 #[test]
4867 fn test_overflow_ends_with_the_owners_next_accepted_push() {
4868 let mut cache = NotificationCache::new();
4869 cache.eviction_marks.overflowed.insert(route());
4870 let unseen = Uri::from("file:///never-published.rs");
4871 assert_eq!(
4872 cache.availability(&unseen, Some(&route())),
4873 DiagnosticsAvailability::Evicted
4874 );
4875
4876 drop(write_error(&mut cache, "file:///fresh.rs"));
4877
4878 assert!(!cache.eviction_marks.has_overflowed(&route()));
4879 assert_eq!(
4880 cache.availability(&unseen, Some(&route())),
4881 DiagnosticsAvailability::Pending
4882 );
4883 }
4884
4885 #[test]
4888 fn test_overflow_ends_with_the_owners_next_accepted_pull_only() {
4889 let mut cache = NotificationCache::new();
4890 cache.eviction_marks.overflowed.insert(route());
4891 let other = ServerId::from_static("other-server");
4892 cache.store_pulled_for_test(&other, &Uri::from("file:///other.rs"), Vec::new());
4893 assert!(cache.eviction_marks.has_overflowed(&route()));
4894
4895 cache.store_pulled_for_test(&route(), &Uri::from("file:///pulled.rs"), Vec::new());
4896 assert!(!cache.eviction_marks.has_overflowed(&route()));
4897 }
4898
4899 #[test]
4903 fn test_overflow_clear_precedes_the_evictions_of_the_same_write() {
4904 let mut cache = overflowed_after_churn();
4905 drop(write_error(&mut cache, "file:///another-new.rs"));
4906 assert!(cache.eviction_marks.has_overflowed(&route()));
4907
4908 drop(write_error(&mut cache, "file:///another-new.rs"));
4909 assert!(!cache.eviction_marks.has_overflowed(&route()));
4910 cache.assert_consistent();
4911 }
4912
4913 #[test]
4917 fn test_overflowing_the_marks_makes_unmarked_files_read_as_evicted_for_that_server() {
4918 let mut cache = full_cache(false);
4919 for i in 0..=MAX_EVICTION_MARKS {
4920 drop(write_error(&mut cache, &format!("file:///churn{i}.rs")));
4921 }
4922 assert!(cache.eviction_marks.has_overflowed(&route()));
4923 assert!(cache.eviction_marks.by_file.len() <= MAX_EVICTION_MARKS);
4924 assert!(cache.eviction_marks.order.len() <= MAX_EVICTION_MARKS);
4925 cache.assert_consistent();
4926
4927 assert_eq!(
4928 cache.availability(&first_uri(), Some(&route())),
4929 DiagnosticsAvailability::Evicted,
4930 "its mark was the oldest, forgotten"
4931 );
4932 let unseen = Uri::from("file:///never-published.rs");
4933 assert_eq!(
4934 cache.availability(&unseen, Some(&route())),
4935 DiagnosticsAvailability::Evicted
4936 );
4937 assert_eq!(
4938 cache.availability(&unseen, Some(&ServerId::from_static("other"))),
4939 DiagnosticsAvailability::Pending
4940 );
4941
4942 drop(cache.clear_server_diagnostics(&route()));
4943 assert_eq!(
4944 cache.availability(&unseen, Some(&route())),
4945 DiagnosticsAvailability::Pending
4946 );
4947 }
4948
4949 #[test]
4950 fn test_evicted_non_empty_entry_is_not_replayable() {
4951 let mut cache = full_cache(false);
4952 evict_one(&mut cache);
4953 assert!(!cache.has_diagnostics(&first_uri()));
4954 assert!(!cache.is_listen_replayable(&first_uri()));
4955 }
4956
4957 #[test]
4958 fn test_recached_entry_leaves_the_replay_ring_view() {
4959 let mut cache = full_cache(true);
4960 evict_one(&mut cache);
4961 let first: Uri = Uri::from("file:///first.rs");
4962 cache.store_diagnostics(&test_server(), &first, None, vec![error_diagnostic()]);
4963 assert!(cache.has_diagnostics(&first_uri()));
4964 assert!(!cache.was_recently_evicted(&first_uri(), std::time::Instant::now()));
4965 }
4966
4967 #[test]
4968 fn test_eviction_replay_expires_after_the_window() {
4969 let mut cache = NotificationCache::new();
4970 let now = std::time::Instant::now();
4971 let gone = Uri::from("file:///gone.rs");
4972 cache.record_empty_eviction(DiagnosticsKey::of(&gone), now);
4973 assert!(cache.was_recently_evicted(&gone, now + EVICTION_REPLAY_WINDOW));
4974 assert!(!cache.was_recently_evicted(
4975 &gone,
4976 now + EVICTION_REPLAY_WINDOW + std::time::Duration::from_secs(1)
4977 ));
4978 }
4979
4980 #[test]
4981 fn test_eviction_record_is_bounded_and_keeps_the_newest() {
4982 let mut cache = NotificationCache::new();
4983 let now = std::time::Instant::now();
4984 for i in 0..MAX_RECENT_EVICTIONS + 44 {
4985 let gone = Uri::from(format!("file:///gone{i}.rs"));
4986 cache.record_empty_eviction(
4987 DiagnosticsKey::of(&gone),
4988 now + std::time::Duration::from_millis(u64::try_from(i).unwrap()),
4989 );
4990 }
4991 assert_eq!(cache.recent_evictions.len(), MAX_RECENT_EVICTIONS);
4992 let at = now + std::time::Duration::from_secs(1);
4993 assert!(!cache.was_recently_evicted(&Uri::from("file:///gone0.rs"), at));
4994 assert!(!cache.was_recently_evicted(&Uri::from("file:///gone43.rs"), at));
4995 assert!(cache.was_recently_evicted(&Uri::from("file:///gone44.rs"), at));
4996 let newest = MAX_RECENT_EVICTIONS + 43;
4997 assert!(cache.was_recently_evicted(&Uri::from(format!("file:///gone{newest}.rs")), at));
4998 }
4999
5000 #[test]
5001 fn test_clearing_a_full_cache_keeps_every_empty_clear_replayable() {
5002 let mut cache = NotificationCache::new();
5003 let server = test_server();
5004 let uris: Vec<Uri> = (0..MAX_DIAGNOSTIC_ENTRIES)
5005 .map(|i| Uri::from(format!("file:///clean{i}.rs")))
5006 .collect();
5007 for uri in &uris {
5008 cache.store_diagnostics(&server, uri, None, vec![]);
5009 }
5010
5011 cache.clear_server_diagnostics(&server);
5012
5013 assert!(uris.iter().all(|uri| cache.is_listen_replayable(uri)));
5014 }
5015
5016 #[test]
5017 fn test_eviction_order_stays_within_twice_the_record_under_churn() {
5018 let mut cache = NotificationCache::new();
5019 let now = std::time::Instant::now();
5020 let keys: Vec<DiagnosticsKey> = (0..3)
5021 .map(|i| DiagnosticsKey::of(&Uri::from(format!("file:///hot{i}.rs"))))
5022 .collect();
5023 for tick in 0..10_000u64 {
5024 let key = keys[usize::try_from(tick % 3).unwrap()].clone();
5025 cache.record_empty_eviction(key, now + std::time::Duration::from_micros(tick));
5026 let record = &cache.recent_evictions;
5027 assert!(record.at.len() <= MAX_RECENT_EVICTIONS);
5028 assert!(record.order.len() <= 2 * record.at.len());
5029 }
5030 assert_eq!(cache.recent_evictions.len(), 3);
5031 }
5032
5033 #[test]
5034 fn test_eviction_record_stays_bounded_under_churn_past_the_cap() {
5035 const KEYS: usize = MAX_RECENT_EVICTIONS + 300;
5036 let mut cache = NotificationCache::new();
5037 let now = std::time::Instant::now();
5038 let keys: Vec<DiagnosticsKey> = (0..KEYS)
5039 .map(|i| DiagnosticsKey::of(&Uri::from(format!("file:///churn{i}.rs"))))
5040 .collect();
5041 for tick in 0..KEYS * 8 {
5042 let key = keys[tick % KEYS].clone();
5043 let at = now + std::time::Duration::from_micros(u64::try_from(tick).unwrap());
5044 cache.record_empty_eviction(key, at);
5045 let record = &cache.recent_evictions;
5046 assert!(record.at.len() <= MAX_RECENT_EVICTIONS);
5047 assert!(record.order.len() <= 2 * record.at.len() + 1);
5048 }
5049 assert_eq!(cache.recent_evictions.len(), MAX_RECENT_EVICTIONS);
5050 }
5051
5052 #[test]
5053 fn test_compaction_keeps_the_oldest_first_expiry_order() {
5054 let mut record = EvictionRecord::default();
5055 let now = std::time::Instant::now();
5056 let a = DiagnosticsKey::of(&Uri::from("file:///a.rs"));
5057 let b = DiagnosticsKey::of(&Uri::from("file:///b.rs"));
5058 record.record(a.clone(), now);
5059 record.record(b.clone(), now + std::time::Duration::from_secs(1));
5060 record.record(a.clone(), now + std::time::Duration::from_secs(2));
5061 record.record(b.clone(), now + std::time::Duration::from_secs(3));
5062 record.compact();
5063 let order: Vec<_> = record.order.iter().map(|(key, _)| key.clone()).collect();
5064 assert_eq!(order, vec![a, b]);
5065 }
5066
5067 #[test]
5068 fn test_eviction_record_overflow_warns_once_per_window() {
5069 use tracing_subscriber::layer::SubscriberExt as _;
5070
5071 let mut record = EvictionRecord::default();
5072 let now = std::time::Instant::now();
5073 let captured = CapturedLogs::default();
5074 let subscriber = tracing_subscriber::registry().with(captured.clone());
5075 let guard = tracing::subscriber::set_default(subscriber);
5076 for i in 0..MAX_RECENT_EVICTIONS + 10 {
5077 let key = DiagnosticsKey::of(&Uri::from(format!("file:///gone{i}.rs")));
5078 record.record(
5079 key,
5080 now + std::time::Duration::from_millis(u64::try_from(i).unwrap()),
5081 );
5082 }
5083 drop(guard);
5084
5085 let warnings = captured
5086 .messages()
5087 .iter()
5088 .filter(|m| m.contains("diagnostics clears evicted"))
5089 .count();
5090 assert_eq!(warnings, 1);
5091 }
5092
5093 #[test]
5094 fn test_eviction_record_deduplicates_and_prunes_expired_entries() {
5095 let mut cache = NotificationCache::new();
5096 let now = std::time::Instant::now();
5097 let a = DiagnosticsKey::of(&Uri::from("file:///a.rs"));
5098 cache.record_empty_eviction(a.clone(), now);
5099 cache.record_empty_eviction(a, now);
5100 assert_eq!(cache.recent_evictions.len(), 1);
5101 let later = now + EVICTION_REPLAY_WINDOW + std::time::Duration::from_secs(1);
5102 cache.record_empty_eviction(DiagnosticsKey::of(&Uri::from("file:///b.rs")), later);
5103 assert_eq!(cache.recent_evictions.len(), 1);
5104 }
5105
5106 #[test]
5107 fn test_alias_eviction_of_an_empty_entry_is_recorded() {
5108 let mut cache = NotificationCache::new();
5109 let server = test_server();
5110 for n in 0..MAX_SOURCES_PER_FILE {
5111 cache.store_published_diagnostics(
5112 &server,
5113 &published(&format!("a{n}.rs")),
5114 None,
5115 vec![],
5116 );
5117 }
5118 assert!(cache.recent_evictions.is_empty());
5119
5120 cache.store_published_diagnostics(
5121 &server,
5122 &PublishedDiagnosticsUri::for_test(file_uri("main.rs"), file_uri("main.rs")),
5123 None,
5124 vec![diagnostic_at(1, "e")],
5125 );
5126
5127 assert!(cache.diagnostics(&file_uri("a0.rs")).is_none());
5128 assert_eq!(cache.recent_evictions.len(), 1);
5129 assert!(cache.is_listen_replayable(&file_uri("main.rs")));
5130 }
5131
5132 #[test]
5133 fn test_clearing_a_server_records_its_empty_entries() {
5134 let mut cache = NotificationCache::new();
5135 let empty = file_uri("empty.rs");
5136 let broken = file_uri("broken.rs");
5137 cache.store_diagnostics(&test_server(), &empty, None, vec![]);
5138 cache.store_diagnostics(&test_server(), &broken, None, vec![error_diagnostic()]);
5139
5140 let cleared = cache.clear_server_diagnostics(&test_server());
5141
5142 assert_eq!(cleared.len(), 2);
5143 assert!(cache.is_listen_replayable(&empty));
5144 assert!(!cache.is_listen_replayable(&broken));
5145 }
5146}