use std::collections::{BTreeMap, HashMap, VecDeque};
use chrono::{DateTime, Utc};
use lsp_types::{Diagnostic as LspDiagnostic, Uri};
use serde::{Deserialize, Serialize};
use tracing::warn;
use crate::config::ServerId;
use crate::util::{truncate_str, truncate_string};
const MAX_LOG_ENTRIES: usize = 100;
const MAX_ENTRY_TEXT_BYTES: usize = 256 * 1024;
const MAX_DIAGNOSTICS_ENTRY_BYTES: usize = 1024 * 1024;
const MAX_DIAGNOSTIC_ENTRIES: usize = 1000;
fn uri_cache_key(uri: &str) -> std::borrow::Cow<'_, str> {
if cfg!(windows) {
std::borrow::Cow::Owned(uri.to_ascii_lowercase())
} else {
std::borrow::Cow::Borrowed(uri)
}
}
const MAX_SERVER_MESSAGES: usize = 50;
const DIAGNOSTIC_ESTIMATE_OVERHEAD_BYTES: usize = 256;
const JSON_ESCAPE_WORST_CASE_FACTOR: usize = 6;
const DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES: usize = 1024;
const fn diagnostic_severity_rank(diagnostic: &LspDiagnostic) -> u8 {
match diagnostic.severity {
Some(lsp_types::DiagnosticSeverity::ERROR) => 0,
Some(lsp_types::DiagnosticSeverity::WARNING) => 1,
Some(lsp_types::DiagnosticSeverity::INFORMATION) => 2,
Some(_) | None => 3,
}
}
fn largest_fitting_prefix(
diagnostics: &[LspDiagnostic],
fits: impl Fn(&[LspDiagnostic]) -> bool,
) -> usize {
let (mut lo, mut hi) = (0usize, diagnostics.len());
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
if fits(&diagnostics[..mid]) {
lo = mid;
} else {
hi = mid - 1;
}
}
lo
}
fn cap_diagnostics_entry_size(uri: &Uri, diagnostics: &mut Vec<LspDiagnostic>) {
let fits = |ds: &[LspDiagnostic]| {
serde_json::to_vec(ds).is_ok_and(|bytes| bytes.len() <= MAX_DIAGNOSTICS_ENTRY_BYTES)
};
let cheaply_estimable = diagnostics.iter().all(|d| {
d.data.is_none()
&& d.code_description.is_none()
&& d.related_information.is_none()
&& d.tags.is_none()
});
if cheaply_estimable {
let estimated: usize = diagnostics
.iter()
.map(|d| {
let raw_string_bytes = d.message.len()
+ d.source.as_deref().map_or(0, str::len)
+ match &d.code {
Some(lsp_types::NumberOrString::String(s)) => s.len(),
_ => 0,
};
raw_string_bytes * JSON_ESCAPE_WORST_CASE_FACTOR
+ DIAGNOSTIC_ESTIMATE_OVERHEAD_BYTES
})
.sum();
if estimated <= MAX_DIAGNOSTICS_ENTRY_BYTES {
return;
}
}
if fits(diagnostics) {
return;
}
let original_count = diagnostics.len();
diagnostics.sort_by_key(diagnostic_severity_rank);
let keep = largest_fitting_prefix(diagnostics, fits).max(1);
diagnostics.truncate(keep);
if diagnostics.len() < original_count {
warn!(
"diagnostics for {} exceeded the {MAX_DIAGNOSTICS_ENTRY_BYTES}-byte cache cap; kept \
the {} highest-severity of {original_count} diagnostics",
uri.as_str(),
diagnostics.len(),
);
}
if diagnostics.len() == 1 && !fits(diagnostics) {
let diagnostic = &mut diagnostics[0];
let had_data = diagnostic.data.is_some();
diagnostic.data = None;
diagnostic.code_description = None;
diagnostic.related_information = None;
diagnostic.tags = None;
warn!(
"diagnostic for {} exceeded the cache cap; dropped its data/code_description/\
related_information/tags fields{}",
uri.as_str(),
if had_data {
" (a later code-action request for this diagnostic may not resolve its quick fix)"
} else {
""
},
);
}
if diagnostics.len() == 1 && !fits(diagnostics) {
let diagnostic = &mut diagnostics[0];
if let Some(source) = &diagnostic.source {
diagnostic.source = Some(truncate_str(source, MAX_ENTRY_TEXT_BYTES));
}
if let Some(lsp_types::NumberOrString::String(code)) = &diagnostic.code {
diagnostic.code = Some(lsp_types::NumberOrString::String(truncate_str(
code,
MAX_ENTRY_TEXT_BYTES,
)));
}
}
if !fits(diagnostics) {
diagnostics.truncate(1);
if let Some(diagnostic) = diagnostics.first_mut() {
diagnostic.message = truncate_str(
&diagnostic.message,
DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES,
);
diagnostic.source = None;
diagnostic.code = None;
diagnostic.code_description = None;
diagnostic.related_information = None;
diagnostic.tags = None;
diagnostic.data = None;
}
warn!(
"diagnostic for {} still exceeded the cache cap after every other mitigation; \
truncated its message to {DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES} bytes and \
cleared all other fields",
uri.as_str(),
);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiagnosticInfo {
pub uri: Uri,
pub version: Option<i32>,
pub diagnostics: Vec<LspDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
pub level: LogLevel,
pub message: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Error,
Warning,
Info,
Debug,
}
impl From<lsp_types::MessageType> for LogLevel {
fn from(msg_type: lsp_types::MessageType) -> Self {
match msg_type {
lsp_types::MessageType::ERROR => Self::Error,
lsp_types::MessageType::WARNING => Self::Warning,
lsp_types::MessageType::INFO => Self::Info,
_ => Self::Debug,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerMessage {
pub message_type: MessageType,
pub message: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MessageType {
Error,
Warning,
Info,
Log,
}
impl From<lsp_types::MessageType> for MessageType {
fn from(msg_type: lsp_types::MessageType) -> Self {
match msg_type {
lsp_types::MessageType::ERROR => Self::Error,
lsp_types::MessageType::WARNING => Self::Warning,
lsp_types::MessageType::INFO => Self::Info,
_ => Self::Log,
}
}
}
#[derive(Debug)]
pub struct NotificationCache {
diagnostics: HashMap<String, DiagnosticInfo>,
diagnostics_owners: HashMap<String, ServerId>,
diagnostic_order: HashMap<ServerId, BTreeMap<u64, String>>,
diagnostic_seq: HashMap<String, u64>,
next_diagnostic_seq: u64,
diagnostics_route_count: usize,
logs: VecDeque<LogEntry>,
messages: VecDeque<ServerMessage>,
}
impl Default for NotificationCache {
fn default() -> Self {
Self::new()
}
}
impl NotificationCache {
#[must_use]
pub fn new() -> Self {
Self {
diagnostics: HashMap::with_capacity(32),
diagnostics_owners: HashMap::with_capacity(32),
diagnostic_order: HashMap::new(),
diagnostic_seq: HashMap::with_capacity(32),
next_diagnostic_seq: 0,
diagnostics_route_count: 1,
logs: VecDeque::with_capacity(MAX_LOG_ENTRIES),
messages: VecDeque::with_capacity(MAX_SERVER_MESSAGES),
}
}
pub fn set_diagnostics_route_count(&mut self, count: usize) {
self.diagnostics_route_count = count.max(1);
}
fn per_server_budget(&self) -> usize {
(MAX_DIAGNOSTIC_ENTRIES / self.diagnostics_route_count.max(1)).max(1)
}
fn server_to_evict_from(&self, writer: &ServerId) -> Option<ServerId> {
let largest = self
.diagnostic_order
.iter()
.filter(|(_, order)| !order.is_empty())
.max_by_key(|(id, order)| (order.len(), id.as_str()));
let budget = self.per_server_budget();
if let Some((id, order)) = largest
&& order.len() > budget
{
return Some(id.clone());
}
if self
.diagnostic_order
.get(writer)
.is_some_and(|order| !order.is_empty())
{
return Some(writer.clone());
}
largest.map(|(id, _)| id.clone())
}
pub fn store_diagnostics(
&mut self,
server_id: &ServerId,
uri: &Uri,
version: Option<i32>,
mut diagnostics: Vec<LspDiagnostic>,
) {
for diagnostic in &mut diagnostics {
diagnostic.message = truncate_string(
std::mem::take(&mut diagnostic.message),
MAX_ENTRY_TEXT_BYTES,
);
}
cap_diagnostics_entry_size(uri, &mut diagnostics);
let key = uri_cache_key(uri.as_str()).into_owned();
let info = DiagnosticInfo {
uri: uri.clone(),
version,
diagnostics,
};
let mut is_new_entry = true;
if let Some(old_seq) = self.diagnostic_seq.remove(&key) {
is_new_entry = false;
if let Some(previous_owner) = self.diagnostics_owners.get(&key)
&& let Some(order) = self.diagnostic_order.get_mut(previous_owner)
{
order.remove(&old_seq);
}
}
if is_new_entry {
while self.diagnostics.len() >= MAX_DIAGNOSTIC_ENTRIES
&& let Some(evict_from) = self.server_to_evict_from(server_id)
&& let Some(order) = self.diagnostic_order.get_mut(&evict_from)
&& let Some((&oldest_seq, oldest_key)) = order.iter().next()
{
let oldest_key = oldest_key.clone();
order.remove(&oldest_seq);
self.diagnostic_seq.remove(&oldest_key);
self.diagnostics_owners.remove(&oldest_key);
self.diagnostics.remove(&oldest_key);
}
}
self.diagnostics_owners
.insert(key.clone(), server_id.clone());
let seq = self.next_diagnostic_seq;
self.next_diagnostic_seq += 1;
self.diagnostic_order
.entry(server_id.clone())
.or_default()
.insert(seq, key.clone());
self.diagnostic_seq.insert(key.clone(), seq);
self.diagnostics.insert(key, info);
}
pub fn store_log(&mut self, level: LogLevel, message: String) {
let entry = LogEntry {
level,
message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
timestamp: Utc::now(),
};
if self.logs.len() >= MAX_LOG_ENTRIES {
self.logs.pop_front();
}
self.logs.push_back(entry);
}
pub fn store_message(&mut self, message_type: MessageType, message: String) {
let msg = ServerMessage {
message_type,
message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
timestamp: Utc::now(),
};
if self.messages.len() >= MAX_SERVER_MESSAGES {
self.messages.pop_front();
}
self.messages.push_back(msg);
}
#[inline]
#[must_use]
pub fn get_diagnostics(&self, uri: &str) -> Option<&DiagnosticInfo> {
self.diagnostics.get(uri_cache_key(uri).as_ref())
}
#[inline]
#[must_use]
pub fn diagnostics_owner(&self, uri: &str) -> Option<&ServerId> {
self.diagnostics_owners.get(uri_cache_key(uri).as_ref())
}
#[inline]
#[must_use]
pub const fn logs(&self) -> &VecDeque<LogEntry> {
&self.logs
}
#[inline]
#[must_use]
pub const fn messages(&self) -> &VecDeque<ServerMessage> {
&self.messages
}
pub fn clear_diagnostics(&mut self, uri: &str) -> Option<DiagnosticInfo> {
let key = uri_cache_key(uri).into_owned();
if let Some(owner) = self.diagnostics_owners.remove(&key)
&& let Some(seq) = self.diagnostic_seq.remove(&key)
&& let Some(order) = self.diagnostic_order.get_mut(&owner)
{
order.remove(&seq);
}
self.diagnostics.remove(&key)
}
pub fn clear_server_diagnostics(&mut self, server_id: &ServerId) {
let Some(order) = self.diagnostic_order.remove(server_id) else {
return;
};
for (_, key) in order {
self.diagnostics.remove(&key);
self.diagnostics_owners.remove(&key);
self.diagnostic_seq.remove(&key);
}
}
pub fn clear_all_diagnostics(&mut self) {
self.diagnostics.clear();
self.diagnostics_owners.clear();
self.diagnostic_order.clear();
self.diagnostic_seq.clear();
}
pub fn clear_logs(&mut self) {
self.logs.clear();
}
pub fn clear_messages(&mut self) {
self.messages.clear();
}
#[inline]
#[must_use]
pub fn diagnostics_count(&self) -> usize {
self.diagnostics.len()
}
#[inline]
#[must_use]
pub fn logs_count(&self) -> usize {
self.logs.len()
}
#[inline]
#[must_use]
pub fn messages_count(&self) -> usize {
self.messages.len()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use lsp_types::{Position, Range};
use super::*;
fn test_server() -> ServerId {
ServerId::from("test-server")
}
#[test]
fn test_notification_cache_new() {
let cache = NotificationCache::new();
assert_eq!(cache.diagnostics_count(), 0);
assert_eq!(cache.logs_count(), 0);
assert_eq!(cache.messages_count(), 0);
}
#[test]
fn test_store_and_get_diagnostics() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let diagnostic = LspDiagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
message: "test error".to_string(),
code: None,
source: None,
code_description: None,
related_information: None,
tags: None,
data: None,
};
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.uri, uri);
assert_eq!(stored.version, Some(1));
assert_eq!(stored.diagnostics.len(), 1);
assert_eq!(stored.diagnostics[0].message, "test error");
}
#[test]
fn test_store_diagnostics_truncates_oversized_message() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
let diagnostic = LspDiagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
message: oversized.clone(),
code: None,
source: None,
code_description: None,
related_information: None,
tags: None,
data: None,
};
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics[0].message;
assert!(stored.len() < oversized.len());
assert!(stored.ends_with("... (truncated)"));
}
fn minimal_diagnostic(message: String) -> LspDiagnostic {
LspDiagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
message,
code: None,
source: None,
code_description: None,
related_information: None,
tags: None,
data: None,
}
}
#[test]
fn test_store_diagnostics_caps_aggregate_size_for_many_small_diagnostics() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let diagnostics: Vec<LspDiagnostic> = (0..5000)
.map(|i| {
minimal_diagnostic(format!(
"diagnostic number {i}, padded: {}",
"x".repeat(200)
))
})
.collect();
let original_count = diagnostics.len();
cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
assert!(
stored.len() < original_count,
"aggregate cap must trim the list, kept {} of {original_count}",
stored.len()
);
assert!(!stored.is_empty(), "must keep at least one diagnostic");
let serialized_len = serde_json::to_vec(stored).unwrap().len();
assert!(
serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
"stored entry must fit the aggregate cap, got {serialized_len} bytes"
);
}
#[test]
fn test_store_diagnostics_truncation_keeps_largest_fitting_prefix() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let diagnostics: Vec<LspDiagnostic> = (0..5000)
.map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
.collect();
cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
assert!(
stored.len() > 2600,
"largest-fitting-prefix search must keep far more than half, kept {}",
stored.len()
);
let serialized_len = serde_json::to_vec(stored).unwrap().len();
assert!(serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES);
let mut with_one_more = stored.clone();
with_one_more.push(minimal_diagnostic(format!(
"diagnostic overflow: {}",
"x".repeat(250)
)));
assert!(
serde_json::to_vec(&with_one_more).unwrap().len() > MAX_DIAGNOSTICS_ENTRY_BYTES,
"kept count must be the largest that fits, not merely a fitting count"
);
}
#[test]
fn test_store_diagnostics_truncation_prefers_higher_severity() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let mut diagnostics: Vec<LspDiagnostic> = (0..5000)
.map(|i| {
let mut d = minimal_diagnostic(format!("hint {i}: {}", "x".repeat(200)));
d.severity = Some(lsp_types::DiagnosticSeverity::HINT);
d
})
.collect();
let mut trailing_error = minimal_diagnostic("the one real error".to_string());
trailing_error.severity = Some(lsp_types::DiagnosticSeverity::ERROR);
diagnostics.push(trailing_error);
cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
assert!(
stored.iter().any(|d| d.message == "the one real error"),
"the trailing ERROR diagnostic must survive truncation over leading HINT noise"
);
}
#[derive(Clone, Default)]
struct CapturedMessages(std::sync::Arc<std::sync::Mutex<Vec<String>>>);
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for CapturedMessages {
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
struct MessageVisitor(String);
impl tracing::field::Visit for MessageVisitor {
fn record_debug(
&mut self,
field: &tracing::field::Field,
value: &dyn std::fmt::Debug,
) {
if field.name() == "message" {
self.0 = format!("{value:?}");
}
}
}
let mut visitor = MessageVisitor(String::new());
event.record(&mut visitor);
self.0.lock().unwrap().push(visitor.0);
}
}
#[test]
fn test_store_diagnostics_warns_when_truncating_list() {
use tracing_subscriber::layer::SubscriberExt as _;
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let diagnostics: Vec<LspDiagnostic> = (0..5000)
.map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
.collect();
let captured = CapturedMessages::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
drop(guard);
let messages = captured.0.lock().unwrap().clone();
assert!(
messages
.iter()
.any(|m| m.contains("highest-severity") && m.contains("file:///test.rs")),
"expected a truncation warning naming the URI, got: {messages:?}"
);
}
#[test]
fn test_store_diagnostics_warns_when_dropping_data_blob() {
use tracing_subscriber::layer::SubscriberExt as _;
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let mut diagnostic = minimal_diagnostic("small message".to_string());
diagnostic.data = Some(serde_json::json!({
"blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
}));
let captured = CapturedMessages::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
drop(guard);
let messages = captured.0.lock().unwrap().clone();
assert!(
messages.iter().any(|m| m.contains("code-action")),
"expected a warning noting the code-action quick-fix impact, got: {messages:?}"
);
}
#[test]
fn test_store_diagnostics_drops_oversized_data_blob_on_single_diagnostic() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let mut diagnostic = minimal_diagnostic("small message".to_string());
diagnostic.data = Some(serde_json::json!({
"blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
}));
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
assert_eq!(stored.len(), 1);
assert_eq!(stored[0].message, "small message");
assert!(
stored[0].data.is_none(),
"oversized data blob must be dropped"
);
let serialized_len = serde_json::to_vec(stored).unwrap().len();
assert!(
serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
"stored entry must fit the aggregate cap after dropping data, got {serialized_len} bytes"
);
}
#[test]
fn test_store_diagnostics_truncates_oversized_source_on_single_diagnostic() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let mut diagnostic = minimal_diagnostic("small message".to_string());
diagnostic.source = Some("x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000));
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
assert_eq!(stored.len(), 1);
assert_eq!(stored[0].message, "small message");
let serialized_len = serde_json::to_vec(stored).unwrap().len();
assert!(
serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
"stored entry must fit the aggregate cap after truncating source, got {serialized_len} bytes"
);
}
#[test]
fn test_store_diagnostics_caps_single_diagnostic_with_every_field_maxed_out() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let mut diagnostic = minimal_diagnostic("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
diagnostic.source = Some("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
diagnostic.code = Some(lsp_types::NumberOrString::String(
"x".repeat(MAX_ENTRY_TEXT_BYTES + 1000),
));
diagnostic.data = Some(serde_json::json!({ "blob": "x".repeat(MAX_ENTRY_TEXT_BYTES) }));
diagnostic.tags = Some(vec![lsp_types::DiagnosticTag::UNNECESSARY; 50]);
diagnostic.related_information = Some(vec![
lsp_types::DiagnosticRelatedInformation {
location: lsp_types::Location {
uri: uri.clone(),
range: Range::default(),
},
message: "x".repeat(1000),
};
5
]);
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
assert_eq!(stored.len(), 1);
let serialized_len = serde_json::to_vec(stored).unwrap().len();
assert!(
serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
"postcondition must hold even with every field maxed out, got {serialized_len} bytes"
);
}
#[test]
fn test_cap_diagnostics_entry_size_terminal_fallback_bounds_untruncated_message() {
let uri: Uri = "file:///test.rs".parse().unwrap();
let mut diagnostics = vec![minimal_diagnostic(
"x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
)];
cap_diagnostics_entry_size(&uri, &mut diagnostics);
assert_eq!(diagnostics.len(), 1);
assert!(
diagnostics[0].message.len() <= DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES + 20,
"terminal fallback must truncate the message itself, got {} bytes",
diagnostics[0].message.len()
);
let serialized_len = serde_json::to_vec(&diagnostics).unwrap().len();
assert!(
serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
"postcondition must hold via the terminal fallback, got {serialized_len} bytes"
);
}
#[test]
fn test_store_diagnostics_cheap_path_leaves_small_diagnostics_untouched() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let mut diagnostic = minimal_diagnostic("a small, ordinary diagnostic message".to_string());
diagnostic.source = Some("rustc".to_string());
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
assert_eq!(stored.len(), 1);
assert_eq!(stored[0].message, "a small, ordinary diagnostic message");
assert_eq!(stored[0].source.as_deref(), Some("rustc"));
}
#[test]
fn test_store_diagnostics_cheap_path_escape_safe_for_control_character_heavy_message() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let nul_heavy_message = "\0".repeat(MAX_ENTRY_TEXT_BYTES);
let diagnostics: Vec<LspDiagnostic> = (0..3)
.map(|_| minimal_diagnostic(nul_heavy_message.clone()))
.collect();
cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
let stored = &cache.get_diagnostics(uri.as_str()).unwrap().diagnostics;
let serialized_len = serde_json::to_vec(stored).unwrap().len();
assert!(
serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
"escape-heavy content must not let the cheap-estimate fast path skip the real cap, \
got {serialized_len} bytes"
);
}
#[test]
fn test_store_diagnostics_replaces_existing() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
assert_eq!(cache.diagnostics_count(), 1);
cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
assert_eq!(cache.diagnostics_count(), 1);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.version, Some(2));
}
#[test]
fn test_clear_diagnostics() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
assert_eq!(cache.diagnostics_count(), 1);
let cleared = cache.clear_diagnostics(uri.as_str());
assert!(cleared.is_some());
assert_eq!(cache.diagnostics_count(), 0);
}
#[test]
fn test_clear_all_diagnostics() {
let mut cache = NotificationCache::new();
let uri1: Uri = "file:///test1.rs".parse().unwrap();
let uri2: Uri = "file:///test2.rs".parse().unwrap();
cache.store_diagnostics(&test_server(), &uri1, Some(1), vec![]);
cache.store_diagnostics(&test_server(), &uri2, Some(1), vec![]);
assert_eq!(cache.diagnostics_count(), 2);
cache.clear_all_diagnostics();
assert_eq!(cache.diagnostics_count(), 0);
}
#[test]
fn test_store_and_get_logs() {
let mut cache = NotificationCache::new();
cache.store_log(LogLevel::Error, "error message".to_string());
cache.store_log(LogLevel::Info, "info message".to_string());
let logs = cache.logs();
assert_eq!(logs.len(), 2);
assert_eq!(logs[0].level, LogLevel::Error);
assert_eq!(logs[0].message, "error message");
assert_eq!(logs[1].level, LogLevel::Info);
assert_eq!(logs[1].message, "info message");
}
#[test]
fn test_logs_max_capacity() {
let mut cache = NotificationCache::new();
for i in 0..MAX_LOG_ENTRIES + 10 {
cache.store_log(LogLevel::Info, format!("message {i}"));
}
assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
let logs = cache.logs();
assert_eq!(logs.front().unwrap().message, "message 10");
assert_eq!(
logs.back().unwrap().message,
format!("message {}", MAX_LOG_ENTRIES + 9)
);
}
#[test]
fn test_store_log_truncates_oversized_message() {
let mut cache = NotificationCache::new();
let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
cache.store_log(LogLevel::Info, oversized.clone());
let stored = &cache.logs()[0].message;
assert!(stored.len() < oversized.len());
assert!(stored.ends_with("... (truncated)"));
}
#[test]
fn test_store_log_does_not_truncate_message_at_or_below_limit() {
let mut cache = NotificationCache::new();
let message = "a".repeat(MAX_ENTRY_TEXT_BYTES);
cache.store_log(LogLevel::Info, message.clone());
assert_eq!(cache.logs()[0].message, message);
}
#[test]
fn test_clear_logs() {
let mut cache = NotificationCache::new();
cache.store_log(LogLevel::Info, "test".to_string());
assert_eq!(cache.logs_count(), 1);
cache.clear_logs();
assert_eq!(cache.logs_count(), 0);
}
#[test]
fn test_store_and_get_messages() {
let mut cache = NotificationCache::new();
cache.store_message(MessageType::Error, "error msg".to_string());
cache.store_message(MessageType::Warning, "warning msg".to_string());
let messages = cache.messages();
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].message_type, MessageType::Error);
assert_eq!(messages[0].message, "error msg");
assert_eq!(messages[1].message_type, MessageType::Warning);
assert_eq!(messages[1].message, "warning msg");
}
#[test]
fn test_messages_max_capacity() {
let mut cache = NotificationCache::new();
for i in 0..MAX_SERVER_MESSAGES + 10 {
cache.store_message(MessageType::Info, format!("message {i}"));
}
assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
let messages = cache.messages();
assert_eq!(messages.front().unwrap().message, "message 10");
assert_eq!(
messages.back().unwrap().message,
format!("message {}", MAX_SERVER_MESSAGES + 9)
);
}
#[test]
fn test_clear_messages() {
let mut cache = NotificationCache::new();
cache.store_message(MessageType::Info, "test".to_string());
assert_eq!(cache.messages_count(), 1);
cache.clear_messages();
assert_eq!(cache.messages_count(), 0);
}
#[test]
fn test_store_message_truncates_oversized_message() {
let mut cache = NotificationCache::new();
let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
cache.store_message(MessageType::Info, oversized.clone());
let stored = &cache.messages()[0].message;
assert!(stored.len() < oversized.len());
assert!(stored.ends_with("... (truncated)"));
}
#[test]
fn test_log_levels() {
let mut cache = NotificationCache::new();
cache.store_log(LogLevel::Error, "error".to_string());
cache.store_log(LogLevel::Warning, "warning".to_string());
cache.store_log(LogLevel::Info, "info".to_string());
cache.store_log(LogLevel::Debug, "debug".to_string());
let logs = cache.logs();
assert_eq!(logs[0].level, LogLevel::Error);
assert_eq!(logs[1].level, LogLevel::Warning);
assert_eq!(logs[2].level, LogLevel::Info);
assert_eq!(logs[3].level, LogLevel::Debug);
}
#[test]
fn test_message_types() {
let mut cache = NotificationCache::new();
cache.store_message(MessageType::Error, "error".to_string());
cache.store_message(MessageType::Warning, "warning".to_string());
cache.store_message(MessageType::Info, "info".to_string());
cache.store_message(MessageType::Log, "log".to_string());
let messages = cache.messages();
assert_eq!(messages[0].message_type, MessageType::Error);
assert_eq!(messages[1].message_type, MessageType::Warning);
assert_eq!(messages[2].message_type, MessageType::Info);
assert_eq!(messages[3].message_type, MessageType::Log);
}
#[test]
fn test_timestamp_ordering() {
let mut cache = NotificationCache::new();
cache.store_log(LogLevel::Info, "first".to_string());
std::thread::sleep(std::time::Duration::from_millis(10));
cache.store_log(LogLevel::Info, "second".to_string());
let logs = cache.logs();
assert!(logs[0].timestamp < logs[1].timestamp);
}
#[test]
fn test_store_diagnostics_empty_list() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let diagnostic = LspDiagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
message: "test error".to_string(),
code: None,
source: None,
code_description: None,
related_information: None,
tags: None,
data: None,
};
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
assert_eq!(
cache
.get_diagnostics(uri.as_str())
.unwrap()
.diagnostics
.len(),
1
);
cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.diagnostics.len(), 0);
assert_eq!(stored.version, Some(2));
}
#[test]
fn test_store_many_diagnostics_single_file() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let diagnostics: Vec<LspDiagnostic> = (0..100)
.map(|i| LspDiagnostic {
range: Range {
start: Position {
line: i,
character: 0,
},
end: Position {
line: i,
character: 10,
},
},
message: format!("Error {i}"),
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
code: None,
source: None,
code_description: None,
related_information: None,
tags: None,
data: None,
})
.collect();
cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.diagnostics.len(), 100);
}
#[test]
fn test_logs_exact_capacity_boundary() {
let mut cache = NotificationCache::new();
for i in 0..MAX_LOG_ENTRIES {
cache.store_log(LogLevel::Info, format!("message {i}"));
}
assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
cache.store_log(LogLevel::Info, "overflow".to_string());
assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
assert_eq!(cache.logs().front().unwrap().message, "message 1");
}
#[test]
fn test_messages_exact_capacity_boundary() {
let mut cache = NotificationCache::new();
for i in 0..MAX_SERVER_MESSAGES {
cache.store_message(MessageType::Info, format!("message {i}"));
}
assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
cache.store_message(MessageType::Info, "overflow".to_string());
assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
assert_eq!(cache.messages().front().unwrap().message, "message 1");
}
#[test]
fn test_diagnostics_max_capacity() {
let mut cache = NotificationCache::new();
for i in 0..MAX_DIAGNOSTIC_ENTRIES + 10 {
let uri: Uri = format!("file:///test{i}.rs").parse().unwrap();
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
}
assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
let evicted: Uri = "file:///test0.rs".parse().unwrap();
assert!(cache.get_diagnostics(evicted.as_str()).is_none());
let newest: Uri = format!("file:///test{}.rs", MAX_DIAGNOSTIC_ENTRIES + 9)
.parse()
.unwrap();
assert!(cache.get_diagnostics(newest.as_str()).is_some());
}
#[test]
fn test_diagnostics_replacing_existing_uri_does_not_trigger_eviction() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///stable.rs".parse().unwrap();
for i in 0..MAX_DIAGNOSTIC_ENTRIES {
cache.store_diagnostics(
&test_server(),
&uri,
Some(i32::try_from(i).unwrap()),
vec![],
);
}
assert_eq!(cache.diagnostics_count(), 1);
assert!(cache.get_diagnostics(uri.as_str()).is_some());
}
#[test]
fn test_diagnostics_republish_refreshes_eviction_order() {
let mut cache = NotificationCache::new();
let actively_edited: Uri = "file:///keep.rs".parse().unwrap();
cache.store_diagnostics(&test_server(), &actively_edited, Some(1), vec![]);
for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
let uri: Uri = format!("file:///untouched{i}.rs").parse().unwrap();
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
}
assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
cache.store_diagnostics(&test_server(), &actively_edited, Some(2), vec![]);
let overflow: Uri = "file:///overflow.rs".parse().unwrap();
cache.store_diagnostics(&test_server(), &overflow, Some(1), vec![]);
assert!(
cache.get_diagnostics(actively_edited.as_str()).is_some(),
"republished entry must survive eviction after being refreshed"
);
let oldest_untouched: Uri = "file:///untouched0.rs".parse().unwrap();
assert!(
cache.get_diagnostics(oldest_untouched.as_str()).is_none(),
"the oldest never-republished entry must be evicted instead"
);
assert!(cache.get_diagnostics(overflow.as_str()).is_some());
}
#[test]
fn test_clear_diagnostics_then_refill_does_not_evict_early() {
let mut cache = NotificationCache::new();
let first: Uri = "file:///first.rs".parse().unwrap();
cache.store_diagnostics(&test_server(), &first, Some(1), vec![]);
cache.clear_diagnostics(first.as_str());
assert_eq!(cache.diagnostics_count(), 0);
for i in 0..MAX_DIAGNOSTIC_ENTRIES {
let uri: Uri = format!("file:///test{i}.rs").parse().unwrap();
cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
}
assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
let first_of_batch: Uri = "file:///test0.rs".parse().unwrap();
assert!(cache.get_diagnostics(first_of_batch.as_str()).is_some());
}
#[test]
fn test_clear_diagnostics_nonexistent() {
let mut cache = NotificationCache::new();
let result = cache.clear_diagnostics("file:///nonexistent.rs");
assert!(result.is_none());
}
#[test]
fn test_store_diagnostics_no_version() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
cache.store_diagnostics(&test_server(), &uri, None, vec![]);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.version, None);
}
#[test]
fn test_noisy_server_does_not_evict_quiet_server_entries() {
let mut cache = NotificationCache::new();
cache.set_diagnostics_route_count(2);
let noisy = ServerId::from("noisy");
let quiet = ServerId::from("quiet");
let quiet_uri: Uri = "file:///quiet/only_file.rs".parse().unwrap();
cache.store_diagnostics(&quiet, &quiet_uri, Some(1), vec![]);
for i in 0..MAX_DIAGNOSTIC_ENTRIES + 50 {
let uri: Uri = format!("file:///noisy/file{i}.rs").parse().unwrap();
cache.store_diagnostics(&noisy, &uri, Some(1), vec![]);
}
assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
assert!(
cache.get_diagnostics(quiet_uri.as_str()).is_some(),
"quiet server's only entry must survive the noisy server's overflow"
);
let noisy_first: Uri = "file:///noisy/file0.rs".parse().unwrap();
assert!(
cache.get_diagnostics(noisy_first.as_str()).is_none(),
"noisy server's own oldest entries must be evicted once the aggregate cache is full"
);
}
#[test]
fn test_dominant_server_exceeds_equal_share_while_others_idle() {
let mut cache = NotificationCache::new();
cache.set_diagnostics_route_count(4);
let dominant = ServerId::from("dominant");
let equal_share = MAX_DIAGNOSTIC_ENTRIES / 4;
let more_than_share = equal_share + 100;
for i in 0..more_than_share {
let uri: Uri = format!("file:///file{i}.rs").parse().unwrap();
cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
}
assert_eq!(
cache.diagnostics_count(),
more_than_share,
"a dominant server must be able to exceed its static equal share while the aggregate has room"
);
for i in more_than_share..MAX_DIAGNOSTIC_ENTRIES {
let uri: Uri = format!("file:///file{i}.rs").parse().unwrap();
cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
}
assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
}
#[test]
fn test_eviction_target_tie_break_is_deterministic() {
let mut cache = NotificationCache::new();
cache.set_diagnostics_route_count(1000);
let a = ServerId::from("a");
let b = ServerId::from("b");
for i in 0..2 {
let uri: Uri = format!("file:///a/file{i}.rs").parse().unwrap();
cache.store_diagnostics(&a, &uri, Some(1), vec![]);
}
for i in 0..2 {
let uri: Uri = format!("file:///b/file{i}.rs").parse().unwrap();
cache.store_diagnostics(&b, &uri, Some(1), vec![]);
}
let writer = ServerId::from("writer");
assert_eq!(cache.server_to_evict_from(&writer), Some(b));
}
#[test]
fn test_new_writer_still_evicts_when_every_existing_server_is_in_share() {
let mut cache = NotificationCache::new();
cache.set_diagnostics_route_count(2);
let a = ServerId::from("a");
let b = ServerId::from("b");
for i in 0..500 {
let uri: Uri = format!("file:///a/file{i}.rs").parse().unwrap();
cache.store_diagnostics(&a, &uri, Some(1), vec![]);
}
for i in 0..500 {
let uri: Uri = format!("file:///b/file{i}.rs").parse().unwrap();
cache.store_diagnostics(&b, &uri, Some(1), vec![]);
}
assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
let c = ServerId::from("c");
let new_uri: Uri = "file:///c/first.rs".parse().unwrap();
cache.store_diagnostics(&c, &new_uri, Some(1), vec![]);
assert_eq!(
cache.diagnostics_count(),
MAX_DIAGNOSTIC_ENTRIES,
"the aggregate cap must still be enforced even when every existing server is within share"
);
assert!(cache.get_diagnostics(new_uri.as_str()).is_some());
let b_oldest: Uri = "file:///b/file0.rs".parse().unwrap();
assert!(
cache.get_diagnostics(b_oldest.as_str()).is_none(),
"the largest in-share server (tie-broken to b) must lose its oldest entry"
);
assert!(
cache.get_diagnostics("file:///a/file0.rs").is_some(),
"the other in-share server must be untouched"
);
}
#[test]
fn test_repeated_writes_same_owner_do_not_grow_order_map() {
let mut cache = NotificationCache::new();
let server = ServerId::from("server");
let uri: Uri = "file:///test.rs".parse().unwrap();
let max_version = i32::try_from(MAX_DIAGNOSTIC_ENTRIES).unwrap() + 10;
for version in 0..max_version {
cache.store_diagnostics(&server, &uri, Some(version), vec![]);
}
assert_eq!(cache.diagnostics_count(), 1);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.version, Some(max_version - 1));
}
#[test]
fn test_store_diagnostics_reassigns_ownership() {
let mut cache = NotificationCache::new();
let old_owner = ServerId::from("old");
let new_owner = ServerId::from("new");
let uri: Uri = "file:///test.rs".parse().unwrap();
cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
assert_eq!(cache.diagnostics_count(), 1);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.version, Some(2));
for i in 0..MAX_DIAGNOSTIC_ENTRIES + 5 {
let other: Uri = format!("file:///old/file{i}.rs").parse().unwrap();
cache.store_diagnostics(&old_owner, &other, Some(1), vec![]);
}
assert!(cache.get_diagnostics(uri.as_str()).is_some());
}
#[test]
fn test_diagnostics_owner_returns_publisher_after_store() {
let mut cache = NotificationCache::new();
let server = ServerId::from("rust");
let uri: Uri = "file:///main.rs".parse().unwrap();
cache.store_diagnostics(&server, &uri, Some(1), vec![]);
assert_eq!(cache.diagnostics_owner(uri.as_str()), Some(&server));
}
#[test]
fn test_diagnostics_owner_none_for_untracked_uri() {
let cache = NotificationCache::new();
let uri: Uri = "file:///never-seen.rs".parse().unwrap();
assert_eq!(cache.diagnostics_owner(uri.as_str()), None);
}
#[test]
fn test_diagnostics_owner_reflects_reassigned_ownership() {
let mut cache = NotificationCache::new();
let old_owner = ServerId::from("old");
let new_owner = ServerId::from("new");
let uri: Uri = "file:///test.rs".parse().unwrap();
cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
assert_eq!(cache.diagnostics_owner(uri.as_str()), Some(&old_owner));
cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
assert_eq!(cache.diagnostics_owner(uri.as_str()), Some(&new_owner));
}
#[test]
fn test_clear_server_diagnostics_scopes_to_one_server() {
let mut cache = NotificationCache::new();
let crashed = ServerId::from("crashed");
let healthy = ServerId::from("healthy");
let crashed_uri: Uri = "file:///crashed/main.py".parse().unwrap();
let healthy_uri: Uri = "file:///healthy/main.rs".parse().unwrap();
cache.store_diagnostics(&crashed, &crashed_uri, Some(1), vec![]);
cache.store_diagnostics(&healthy, &healthy_uri, Some(1), vec![]);
cache.clear_server_diagnostics(&crashed);
assert!(cache.get_diagnostics(crashed_uri.as_str()).is_none());
assert!(cache.get_diagnostics(healthy_uri.as_str()).is_some());
assert_eq!(cache.diagnostics_count(), 1);
cache.clear_server_diagnostics(&crashed);
assert_eq!(cache.diagnostics_count(), 1);
}
#[test]
fn test_shrinking_budget_affects_eviction_target_not_existing_entries() {
let mut cache = NotificationCache::new();
let server = ServerId::from("server");
for i in 0..MAX_DIAGNOSTIC_ENTRIES {
let uri: Uri = format!("file:///file{i}.rs").parse().unwrap();
cache.store_diagnostics(&server, &uri, Some(1), vec![]);
}
assert_eq!(
cache.diagnostics_count(),
MAX_DIAGNOSTIC_ENTRIES,
"filling to the aggregate cap must not evict anything early"
);
cache.set_diagnostics_route_count(4);
assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
let other = ServerId::from("other");
let new_uri: Uri = "file:///other/new.rs".parse().unwrap();
cache.store_diagnostics(&other, &new_uri, Some(1), vec![]);
assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
assert!(cache.get_diagnostics(new_uri.as_str()).is_some());
let server_oldest: Uri = "file:///file0.rs".parse().unwrap();
assert!(
cache.get_diagnostics(server_oldest.as_str()).is_none(),
"the pre-existing server's oldest entry, now far over its shrunk share, must be evicted"
);
}
}