use lspz::codec::compact::{CompactDiagnostics, compress, decompress};
use lspz::codec::toon;
use lspz::interceptors::diagnostics::DiagnosticsCompressor;
use lspz::interceptors::{Direction, Interceptor};
fn gopls_diagnostics() -> serde_json::Value {
let mut diags = Vec::new();
let unused_code = "UnusedVar";
let import_code = "UnusedImport";
let var_names = [
"unusedVar",
"anotherVar",
"x",
"temp",
"result",
"count",
"index",
"value",
"data",
];
for (i, name) in var_names.iter().enumerate() {
diags.push(serde_json::json!({
"range": {
"start": {"line": i * 2, "character": 5},
"end": {"line": i * 2, "character": 5 + name.len() as u64}
},
"severity": 2,
"message": format!("declared and not used: {}", name),
"code": unused_code,
"source": "gopls",
}));
}
let imports = ["os", "fmt"];
for (i, pkg) in imports.iter().enumerate() {
diags.push(serde_json::json!({
"range": {
"start": {"line": 20 + i, "character": 0},
"end": {"line": 20 + i, "character": 10}
},
"severity": 2,
"message": format!("\"{}\" imported and not used", pkg),
"code": import_code,
"source": "gopls",
}));
}
diags.push(serde_json::json!({
"range": {
"start": {"line": 30, "character": 5},
"end": {"line": 30, "character": 15}
},
"severity": 1,
"message": "cannot use str (type string) as type int in assignment",
"code": "Assignment",
"source": "gopls",
}));
diags.push(serde_json::json!({
"range": {
"start": {"line": 35, "character": 10},
"end": {"line": 35, "character": 20}
},
"severity": 2,
"message": "param paramName is unused",
"code": "unusedparam",
"source": "gopls",
}));
serde_json::json!({
"uri": "file:///test/main.go",
"diagnostics": diags,
})
}
fn rust_analyzer_diagnostics() -> serde_json::Value {
serde_json::json!({
"uri": "file:///test/src/main.rs",
"diagnostics": [
{
"range": {"start": {"line": 2, "character": 8}, "end": {"line": 2, "character": 13}},
"severity": 2,
"message": "unused variable: `x`",
"code": "unused_variables",
"source": "rust-analyzer"
},
{
"range": {"start": {"line": 3, "character": 8}, "end": {"line": 3, "character": 13}},
"severity": 2,
"message": "unused variable: `y`",
"code": "unused_variables",
"source": "rust-analyzer"
},
{
"range": {"start": {"line": 5, "character": 13}, "end": {"line": 5, "character": 22}},
"severity": 1,
"message": "cannot find value `unknown_fn` in this scope",
"code": "E0425",
"source": "rust-analyzer"
}
]
})
}
#[test]
fn test_gopls_compression_roundtrip() {
let params = gopls_diagnostics();
let original_count = params["diagnostics"].as_array().unwrap().len();
let compact_val = compress(¶ms).expect("gopls compression should succeed");
let compact: CompactDiagnostics =
serde_json::from_value(compact_val.clone()).expect("should deserialize");
let decompressed = decompress(&compact_val).expect("decompression should succeed");
let decompressed_count = decompressed["diagnostics"].as_array().unwrap().len();
assert!(
decompressed_count >= original_count,
"decompressed count {} should be >= original {}",
decompressed_count,
original_count
);
assert_eq!(compact.uri, "file:///test/main.go");
assert_eq!(compact.version, 1);
}
#[tokio::test]
async fn test_gopls_dedup_with_normalization() {
let compressor = DiagnosticsCompressor::default();
let params = gopls_diagnostics();
let result = compressor
.intercept(
"textDocument/publishDiagnostics",
params,
Direction::ServerToClient,
)
.await
.expect("compression should succeed");
let compact = result.expect("should produce output");
let diags = compact["diagnostics"].as_array().unwrap();
assert_eq!(
diags.len(),
4,
"gopls 13 diags → 4 groups with normalization"
);
let unused_var = diags.iter().find(|d| d["m"] == "unused variable").unwrap();
assert_eq!(unused_var["n"], 9, "unused variable count should be 9");
assert_eq!(unused_var["s"], "W");
}
#[test]
fn test_rust_analyzer_compression_roundtrip() {
let params = rust_analyzer_diagnostics();
let original_count = params["diagnostics"].as_array().unwrap().len();
let compact_val = compress(¶ms).expect("rust-analyzer compression should succeed");
let decompressed = decompress(&compact_val).expect("decompression should succeed");
let decompressed_count = decompressed["diagnostics"].as_array().unwrap().len();
assert!(
decompressed_count >= original_count,
"rust-analyzer: decompressed {} >= original {}",
decompressed_count,
original_count
);
assert_eq!(decompressed["uri"], "file:///test/src/main.rs");
}
#[tokio::test]
async fn test_rust_analyzer_dedup() {
let compressor = DiagnosticsCompressor::default();
let params = rust_analyzer_diagnostics();
let result = compressor
.intercept(
"textDocument/publishDiagnostics",
params,
Direction::ServerToClient,
)
.await
.expect("compression should succeed");
let compact = result.expect("should produce output");
let diags = compact["diagnostics"].as_array().unwrap();
assert_eq!(diags.len(), 2, "rust-analyzer 3 diags → 2 groups");
}
fn basedpyright_diagnostics() -> serde_json::Value {
serde_json::json!({
"uri": "file:///test/src/main.py",
"diagnostics": [
{
"range": {"start": {"line": 1, "character": 0}, "end": {"line": 1, "character": 10}},
"severity": 1,
"message": "Argument of type 'int' is not assignable to parameter of type 'str'",
"code": "reportGeneralTypeIssues",
"source": "basedpyright"
},
{
"range": {"start": {"line": 3, "character": 0}, "end": {"line": 3, "character": 14}},
"severity": 2,
"message": "\"os\" is not accessed",
"code": "reportUnusedImport",
"source": "basedpyright"
},
{
"range": {"start": {"line": 4, "character": 0}, "end": {"line": 4, "character": 16}},
"severity": 2,
"message": "\"typing\" is not accessed",
"code": "reportUnusedImport",
"source": "basedpyright"
},
{
"range": {"start": {"line": 6, "character": 4}, "end": {"line": 6, "character": 10}},
"severity": 2,
"message": "Variable \"result\" is not used",
"code": "reportUnusedVariable",
"source": "basedpyright"
},
{
"range": {"start": {"line": 7, "character": 4}, "end": {"line": 7, "character": 8}},
"severity": 2,
"message": "Variable \"temp\" is not used",
"code": "reportUnusedVariable",
"source": "basedpyright"
},
{
"range": {"start": {"line": 9, "character": 0}, "end": {"line": 9, "character": 1}},
"severity": 1,
"message": "Name \"undefined_var\" is not defined",
"code": "reportUndefinedVariable",
"source": "basedpyright"
},
]
})
}
#[test]
fn test_basedpyright_compression_roundtrip() {
let params = basedpyright_diagnostics();
let original_count = params["diagnostics"].as_array().unwrap().len();
let compact_val = compress(¶ms).expect("basedpyright compression should succeed");
let compact: CompactDiagnostics =
serde_json::from_value(compact_val.clone()).expect("should deserialize");
let decompressed = decompress(&compact_val).expect("decompression should succeed");
let decompressed_count = decompressed["diagnostics"].as_array().unwrap().len();
assert!(
decompressed_count >= original_count,
"basedpyright: decompressed {} >= original {}",
decompressed_count,
original_count
);
assert_eq!(compact.uri, "file:///test/src/main.py");
assert_eq!(compact.version, 1);
}
#[tokio::test]
async fn test_basedpyright_dedup() {
let compressor = DiagnosticsCompressor::default();
let params = basedpyright_diagnostics();
let result = compressor
.intercept(
"textDocument/publishDiagnostics",
params,
Direction::ServerToClient,
)
.await
.expect("compression should succeed");
let compact = result.expect("should produce output");
let diags = compact["diagnostics"].as_array().unwrap();
assert_eq!(
diags.len(),
4,
"basedpyright: expected 4 groups, got {}",
diags.len()
);
}
fn typescript_diagnostics() -> serde_json::Value {
serde_json::json!({
"uri": "file:///test/src/app.ts",
"diagnostics": [
{
"range": {"start": {"line": 1, "character": 7}, "end": {"line": 1, "character": 12}},
"severity": 1,
"message": "Type 'number' is not assignable to type 'string'.",
"code": 2322,
"source": "ts"
},
{
"range": {"start": {"line": 3, "character": 7}, "end": {"line": 3, "character": 16}},
"severity": 1,
"message": "Type 'null' is not assignable to type 'string'.",
"code": 2322,
"source": "ts"
},
{
"range": {"start": {"line": 5, "character": 10}, "end": {"line": 5, "character": 14}},
"severity": 2,
"message": "'temp' is declared but its value is never read.",
"code": 6133,
"source": "ts"
},
{
"range": {"start": {"line": 6, "character": 10}, "end": {"line": 6, "character": 15}},
"severity": 2,
"message": "'count' is declared but its value is never read.",
"code": 6133,
"source": "ts"
},
{
"range": {"start": {"line": 8, "character": 1}, "end": {"line": 8, "character": 6}},
"severity": 1,
"message": "Property 'nonExistent' does not exist on type 'MyType'.",
"code": 2339,
"source": "ts"
},
{
"range": {"start": {"line": 10, "character": 0}, "end": {"line": 10, "character": 17}},
"severity": 1,
"message": "Cannot find name 'unknownFn'. Did you mean 'knownFn'?",
"code": 2552,
"source": "ts"
},
]
})
}
#[test]
fn test_typescript_compression_roundtrip() {
let params = typescript_diagnostics();
let original_count = params["diagnostics"].as_array().unwrap().len();
let compact_val = compress(¶ms).expect("typescript compression should succeed");
let compact: CompactDiagnostics =
serde_json::from_value(compact_val.clone()).expect("should deserialize");
let decompressed = decompress(&compact_val).expect("decompression should succeed");
let decompressed_count = decompressed["diagnostics"].as_array().unwrap().len();
assert!(
decompressed_count >= original_count,
"typescript: decompressed {} >= original {}",
decompressed_count,
original_count
);
assert_eq!(compact.uri, "file:///test/src/app.ts");
assert_eq!(compact.version, 1);
}
#[tokio::test]
async fn test_typescript_dedup() {
let compressor = DiagnosticsCompressor::default();
let params = typescript_diagnostics();
let result = compressor
.intercept(
"textDocument/publishDiagnostics",
params,
Direction::ServerToClient,
)
.await
.expect("compression should succeed");
let compact = result.expect("should produce output");
let diags = compact["diagnostics"].as_array().unwrap();
assert_eq!(
diags.len(),
4,
"typescript: expected 4 groups, got {}",
diags.len()
);
}
#[tokio::test]
async fn test_token_savings_gopls() {
use tiktoken_rs::cl100k_base;
let bpe = cl100k_base().expect("should load tokenizer");
let params = gopls_diagnostics();
let original_json = serde_json::to_string(¶ms).expect("should serialize");
let compact_basic = compress(¶ms).expect("should compress");
let compact_basic_json = serde_json::to_string(&compact_basic).expect("should serialize");
let compressor = DiagnosticsCompressor::default();
let compact_norm = compressor
.intercept(
"textDocument/publishDiagnostics",
params,
Direction::ServerToClient,
)
.await
.expect("should compress");
let compact_norm_json = serde_json::to_string(&compact_norm.expect("should produce output"))
.expect("should serialize");
let original_tokens = bpe.encode_with_special_tokens(&original_json).len();
let basic_tokens = bpe.encode_with_special_tokens(&compact_basic_json).len();
let norm_tokens = bpe.encode_with_special_tokens(&compact_norm_json).len();
let basic_savings = 1.0 - (basic_tokens as f64 / original_tokens as f64);
let norm_savings = 1.0 - (norm_tokens as f64 / original_tokens as f64);
println!("─── gopls Token Savings ───");
println!("Original tokens: {}", original_tokens);
println!(
"Basic compression: {} ({:.1}% savings)",
basic_tokens,
basic_savings * 100.0
);
println!(
"With normalization: {} ({:.1}% savings)",
norm_tokens,
norm_savings * 100.0
);
assert!(
basic_savings >= 0.30,
"Basic compression should save ≥30%, got {:.1}%",
basic_savings * 100.0
);
assert!(
norm_savings >= 0.60,
"Normalization + dedup should save ≥60%, got {:.1}%",
norm_savings * 100.0
);
}
#[tokio::test]
async fn test_token_savings_rust_analyzer() {
use tiktoken_rs::cl100k_base;
let bpe = cl100k_base().expect("should load tokenizer");
let params = rust_analyzer_diagnostics();
let original_json = serde_json::to_string(¶ms).expect("should serialize");
let compressor = DiagnosticsCompressor::default();
let compact = compressor
.intercept(
"textDocument/publishDiagnostics",
params,
Direction::ServerToClient,
)
.await
.expect("should compress");
let compact_json =
serde_json::to_string(&compact.expect("should produce output")).expect("should serialize");
let original_tokens = bpe.encode_with_special_tokens(&original_json).len();
let compressed_tokens = bpe.encode_with_special_tokens(&compact_json).len();
let savings = 1.0 - (compressed_tokens as f64 / original_tokens as f64);
println!("─── rust-analyzer Token Savings ───");
println!("Original tokens: {}", original_tokens);
println!("Compressed tokens: {}", compressed_tokens);
println!("Savings: {:.1}%", savings * 100.0);
assert!(
savings >= 0.40,
"rust-analyzer compression should save ≥40%, got {:.1}%",
savings * 100.0
);
}
#[tokio::test]
async fn test_normalization_increases_dedup() {
let with_norm = DiagnosticsCompressor::default();
let without_norm = DiagnosticsCompressor {
enable_normalisation: false,
};
let params = gopls_diagnostics();
let with_result = with_norm
.intercept(
"textDocument/publishDiagnostics",
params.clone(),
Direction::ServerToClient,
)
.await
.expect("should compress");
let without_result = without_norm
.intercept(
"textDocument/publishDiagnostics",
params,
Direction::ServerToClient,
)
.await
.expect("should compress");
let with_groups = with_result.unwrap()["diagnostics"]
.as_array()
.unwrap()
.len();
let without_groups = without_result.unwrap()["diagnostics"]
.as_array()
.unwrap()
.len();
assert!(
with_groups < without_groups,
"Normalization should reduce groups: {} < {}",
with_groups,
without_groups
);
}
async fn compress_and_toon(params: serde_json::Value) -> String {
let compressor = DiagnosticsCompressor::default();
let compact = compressor
.intercept(
"textDocument/publishDiagnostics",
params,
Direction::ServerToClient,
)
.await
.expect("compression should succeed")
.expect("should produce output");
let typed: CompactDiagnostics =
serde_json::from_value(compact).expect("should deserialize to CompactDiagnostics");
toon::diagnostics_to_toon(&typed)
}
#[tokio::test]
async fn test_toon_uses_self_explanatory_field_names() {
let toon = compress_and_toon(gopls_diagnostics()).await;
assert!(
toon.contains("warning") || toon.contains("error"),
"TOON should contain 'warning' or 'error', got: {}",
toon
);
let lines: Vec<&str> = toon.lines().collect();
for line in &lines {
if line.starts_with(" ") {
let severity = line.split(',').next().unwrap_or("").trim();
assert!(
severity == "warning"
|| severity == "error"
|| severity == "info"
|| severity == "hint",
"TOON severity should be full word, got: '{:?}'",
severity
);
}
}
assert!(
toon.contains("severity,message,code,range,count"),
"TOON table header should use full field names"
);
}
#[tokio::test]
async fn test_toon_no_compact_abbreviations() {
let toon = compress_and_toon(gopls_diagnostics()).await;
assert!(
!toon.contains("\"m\":"),
"TOON should not contain compact 'm:' field"
);
assert!(
!toon.contains("\"s\":"),
"TOON should not contain compact 's:' field"
);
assert!(
!toon.contains("\"r\":"),
"TOON should not contain compact 'r:' field"
);
assert!(
!toon.contains("\"c\":"),
"TOON should not contain compact 'c:' field"
);
}
#[tokio::test]
async fn test_toon_gopls() {
let toon = compress_and_toon(gopls_diagnostics()).await;
assert!(toon.contains("uri: file:///test/main.go"));
assert!(
toon.contains("diagnostics[4]{"),
"gopls 13 diags → 4 groups, header: {:?}",
toon.lines().find(|l| l.starts_with("diagnostics"))
);
assert!(toon.contains("warning"), "should have warning entries");
}
#[tokio::test]
async fn test_toon_rust_analyzer() {
let toon = compress_and_toon(rust_analyzer_diagnostics()).await;
assert!(toon.contains("uri: file:///test/src/main.rs"));
assert!(
toon.contains("diagnostics[2]{"),
"rust-analyzer 3 diags → 2 groups"
);
assert!(toon.contains("unused variable"));
assert!(toon.contains("cannot find value"));
}
#[tokio::test]
async fn test_toon_basedpyright() {
let toon = compress_and_toon(basedpyright_diagnostics()).await;
assert!(toon.contains("uri: file:///test/src/main.py"));
assert!(toon.contains("diagnostics[4]{"));
assert!(toon.contains("not assignable"));
}
#[tokio::test]
async fn test_toon_typescript() {
let toon = compress_and_toon(typescript_diagnostics()).await;
assert!(toon.contains("uri: file:///test/src/app.ts"));
assert!(toon.contains("diagnostics[4]{"));
assert!(toon.contains("type mismatch"));
assert!(toon.contains("unused variable"));
assert!(toon.contains("missing property"));
assert!(toon.contains("unresolved reference"));
}
#[tokio::test]
async fn test_toon_range_format() {
let toon = compress_and_toon(rust_analyzer_diagnostics()).await;
for line in toon.lines() {
if line.contains("warning") || line.contains("error") {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() >= 4 {
let range = parts[3].trim();
assert!(
range.contains(':') && range.contains('-'),
"Range should be L:C-L:C, got: '{}'",
range
);
}
}
}
}
#[tokio::test]
async fn test_toon_empty_diagnostics() {
let params = serde_json::json!({
"uri": "file:///empty.rs",
"diagnostics": []
});
let toon = compress_and_toon(params).await;
assert!(toon.contains("diagnostics[0]{"));
assert!(toon.contains("uri: file:///empty.rs"));
}