use crate::tests::response_probe::dump_path;
use serial_test::serial;
use std::collections::BTreeSet;
const BASELINE: &str = include_str!("response_shape_baseline.tsv");
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum Category {
Unit,
Bool,
Int,
Float,
Str,
Collection,
Custom,
Any,
}
struct Declared {
category: Category,
optional: bool,
}
fn categorize(declared: &str) -> Declared {
let declared = declared.trim();
if let Some(inner) = declared
.strip_prefix("Option<")
.and_then(|d| d.strip_suffix('>'))
{
return Declared {
category: categorize(inner).category,
optional: true,
};
}
Declared {
category: category_of(declared),
optional: false,
}
}
fn category_of(declared: &str) -> Category {
if declared.starts_with('(') && declared != "()" {
return Category::Collection;
}
if let Some((head, _)) = declared.split_once('<') {
return match head {
"Vec" | "VecDeque" | "HashSet" | "BTreeSet" | "HashMap" | "BTreeMap" => {
Category::Collection
}
_ => Category::Custom,
};
}
match declared {
"()" => Category::Unit,
"bool" => Category::Bool,
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128"
| "isize" => Category::Int,
"f32" | "f64" => Category::Float,
"String" | "BulkString" | "str" | "char" => Category::Str,
"Value" => Category::Any,
_ => Category::Custom,
}
}
fn accepts(declared: &Declared, kind: &str) -> bool {
let base = kind.strip_prefix("Empty").unwrap_or(kind);
if base == "Null" {
return declared.optional || matches!(declared.category, Category::Unit | Category::Any);
}
match declared.category {
Category::Any => true,
Category::Unit => kind == "SimpleString(OK)",
Category::Bool => matches!(
kind,
"Integer(0)" | "Integer(1)" | "Boolean" | "SimpleString(OK)"
),
Category::Int => base.starts_with("Integer") || base == "BulkString",
Category::Float => base.starts_with("Integer") || matches!(base, "Double" | "BulkString"),
Category::Str => matches!(base, "BulkString" | "SimpleString" | "SimpleString(OK)"),
Category::Collection => matches!(base, "Array" | "IntegerArray" | "Set" | "Map" | "Push"),
Category::Custom => matches!(
base,
"Array" | "Map" | "Set" | "BulkString" | "SimpleString" | "SimpleString(OK)"
),
}
}
struct Row {
command: String,
declared: String,
kind: String,
decoded: bool,
}
fn parse(content: &str) -> Vec<Row> {
content
.lines()
.map(str::trim_end)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| {
let mut fields = line.split('\t');
Some(Row {
command: fields.next()?.to_owned(),
declared: fields.next()?.to_owned(),
kind: fields.next()?.trim().to_owned(),
decoded: fields.next().is_none_or(|o| o.trim() != "refused"),
})
})
.collect()
}
fn key(row: &Row) -> String {
format!("{}\t{}\t{}", row.command, row.declared, row.kind)
}
#[test]
#[serial]
fn response_shape_report() {
if std::env::var("RUSTIS_RESPONSE_SHAPE_REPORT").is_err() {
println!(
"skipped: set RUSTIS_RESPONSE_SHAPE_REPORT=1 and run after a full suite run, \
which is what fills {}",
dump_path()
);
return;
}
let path = dump_path();
let Ok(content) = std::fs::read_to_string(&path) else {
println!("no probe dump at {path}: run the whole suite first, it is what writes it");
return;
};
let rows = parse(&content);
if rows.is_empty() {
println!("{path} holds no observation: the suite recorded nothing");
return;
}
let accepted: BTreeSet<String> = parse(BASELINE).iter().map(key).collect();
let mut unexplained = Vec::new();
for row in &rows {
if row.kind == "Error" {
continue;
}
if !row.decoded {
continue;
}
if accepts(&categorize(&row.declared), &row.kind) {
continue;
}
if accepted.contains(&key(row)) {
continue;
}
unexplained.push(row);
}
println!(
"{} observations, {} unexplained",
rows.len(),
unexplained.len()
);
if !unexplained.is_empty() {
let mut report = String::from(
"declared response types the server's reply contradicts.\n\
Read each against COMMAND DOCS or the raw reply, then either fix the \
type or add the row to response_shape_baseline.tsv with its reason.\n\n",
);
for row in &unexplained {
report.push_str(&format!(
" {:<32} {:<40} answered {}\n",
row.command, row.declared, row.kind
));
}
println!("{report}");
}
}