use redis::ConnectionLike;
use std::collections::HashMap;
pub const REDIS_CE_6_0: Component = ("redis", (6, 0, 0));
pub const REDIS_CE_7_0: Component = ("redis", (7, 0, 0));
pub const REDIS_CE_7_2: Component = ("redis", (7, 2, 0));
pub const REDIS_CE_7_4: Component = ("redis", (7, 4, 0));
pub const REDIS_CE_8_0: Component = ("redis", (8, 0, 0));
pub const REDIS_CE_8_2: Component = ("redis", (8, 1, 240));
pub const REDIS_CE_8_4: Component = ("redis", (8, 3, 224));
pub const REDIS_CE_8_6: Component = ("redis", (8, 6, 0));
pub const REDIS_BLOOM_ANY: Component = ("redis:bf", (0, 0, 0));
pub const VALKEY_8_1: Component = ("valkey", (8, 1, 0));
pub const VALKEY_9_0: Component = ("valkey", (9, 0, 0));
pub const VALKEY_9_1: Component = ("valkey", (9, 1, 0));
pub type Version = (u32, u32, u32);
pub type Component<'a> = (&'a str, Version);
pub struct ComponentMatcher<'a> {
conjunctive_parts: Vec<Vec<Component<'a>>>,
}
impl<'a> ComponentMatcher<'a> {
pub fn matches(&self, available_components: &AvailableComponents) -> bool {
self.conjunctive_parts.iter().all(|disjunctive_parts| {
disjunctive_parts
.iter()
.any(|component| available_components.supports(*component))
})
}
}
impl<'a> From<Component<'a>> for ComponentMatcher<'a> {
fn from(value: Component<'a>) -> Self {
Self {
conjunctive_parts: vec![vec![value]],
}
}
}
impl<'a> From<&[Component<'a>]> for ComponentMatcher<'a> {
fn from(value: &[Component<'a>]) -> Self {
Self {
conjunctive_parts: vec![value.to_vec()],
}
}
}
impl<'a> From<&[&[Component<'a>]]> for ComponentMatcher<'a> {
fn from(value: &[&[Component<'a>]]) -> Self {
Self {
conjunctive_parts: value
.iter()
.map(|disjunctive_part| disjunctive_part.to_vec())
.collect(),
}
}
}
macro_rules! matcher_array_impls {
($n:expr) => {
impl<'a> From<[Component<'a>; $n]> for ComponentMatcher<'a> {
fn from(value: [Component<'a>; $n]) -> Self {
let coerced_value: &[Component<'a>] = &value;
Self::from(coerced_value)
}
}
impl<'a> From<[&[Component<'a>]; $n]> for ComponentMatcher<'a> {
fn from(value: [&[Component<'a>]; $n]) -> Self {
let coerced_value: &[&[Component<'a>]] = &value;
Self::from(coerced_value)
}
}
};
}
matcher_array_impls!(1);
matcher_array_impls!(2);
matcher_array_impls!(3);
#[derive(Clone)]
pub struct AvailableComponents {
components: HashMap<String, Version>,
}
impl AvailableComponents {
fn parse_info(info_response: &str) -> HashMap<String, Version> {
let mut ret = HashMap::new();
for raw_line in info_response.lines() {
let line = raw_line.split("#").next().unwrap();
let mut split = line.splitn(2, ":");
let (Some(key), Some(value)) = (split.next(), split.next()) else {
continue;
};
let Some((mut name, version)) = Self::parse_info_kv(key.trim(), value.trim()) else {
continue;
};
if name == "bf" {
if version > (8, 0, 0) {
name = "redis:bf".to_string();
} else {
name = "valkey:bf".to_string();
}
}
ret.insert(name, version);
}
ret
}
fn parse_info_kv(key: &str, value: &str) -> Option<(String, Version)> {
if key.ends_with("_version") {
let name = &key[0..key.len() - 8];
return Some((name.to_owned(), Self::extract_version(value)));
}
if key == "module" {
let mut options = HashMap::new();
for pair in value.split(",") {
let mut inner_split = pair.splitn(2, "=");
match (inner_split.next(), inner_split.next()) {
(Some(key), Some(value)) => {
options.insert(key.trim(), value.trim());
}
_ => continue,
}
}
let name = options.get("name")?;
let version_str = options.get("ver")?;
return Some((name.to_string(), Self::extract_version(version_str)));
}
None
}
fn extract_version(value: &str) -> Version {
let number_str = value.split("-").next().unwrap();
let numbers = number_str
.split('.')
.map(|version| version.parse::<u32>().unwrap())
.collect::<Vec<_>>();
let (major, minor, patch) = match numbers.as_slice() {
[number] => {
let mut rest = *number;
let patch = rest % 100;
rest = (rest - patch) / 100;
let minor = rest % 100;
let major = (rest - minor) / 100;
(major, minor, patch)
}
[major, minor, patch] => (*major, *minor, *patch),
_ => panic!(
"version number extraction not implemented for {} parts of '{}'",
numbers.len(),
value
),
};
(major, minor, patch)
}
pub fn supports(&self, component: Component) -> bool {
let (name, requested_version) = component;
let Some(available_version) = self.components.get(name) else {
return false;
};
available_version >= &requested_version
}
}
impl<C: ConnectionLike> From<&mut C> for AvailableComponents {
fn from(conn: &mut C) -> Self {
let info_response: String = redis::Cmd::new().arg("INFO").query(conn).unwrap();
let components = Self::parse_info(info_response.as_str());
Self { components }
}
}
impl<'a> From<&'a AvailableComponents> for Vec<Component<'a>> {
fn from(value: &'a AvailableComponents) -> Self {
value
.components
.iter()
.map(|(name, version)| (name.as_str(), *version))
.collect::<Vec<Component<'a>>>()
}
}
pub trait TestContextVersioning {
fn get_available_components(&self) -> AvailableComponents;
fn supports<'a, T: Into<ComponentMatcher<'a>>>(&self, into_matcher: T) -> bool {
into_matcher
.into()
.matches(&self.get_available_components())
}
}
#[macro_export]
macro_rules! skip_if_context_does_not_support {
($ctx:expr, $component:expr) => {{ $crate::skip_if_context_does_not_support!($ctx, $component, ()) }};
($ctx:expr, $component:expr, $ret:expr) => {{
if !$ctx.supports($component) {
eprintln!(
"Skipping the test because the running server does not support {:?}.",
$component
);
return $ret;
}
}};
}
#[macro_export]
macro_rules! run_test_if_version_supported {
($component:expr) => {{
let ctx = $crate::support::TestContext::new();
$crate::skip_if_context_does_not_support!(ctx, $component);
ctx
}};
}