use super::*;
use crate::nodes::{NodeOperators, StreamOperators, constant};
use crate::types::Burst;
use crate::{RunFor, RunMode, burst};
use futures::StreamExt;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use testcontainers::{GenericImage, core::WaitFor, runners::SyncRunner};
const REDIS_PORT: u16 = 6379;
const REDIS_IMAGE: &str = "redis";
const REDIS_TAG: &str = "7-alpine";
fn start_redis() -> anyhow::Result<(impl Drop, String)> {
let container = GenericImage::new(REDIS_IMAGE, REDIS_TAG)
.with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
.start()?;
let port = container.get_host_port_ipv4(REDIS_PORT)?;
let url = format!("redis://127.0.0.1:{port}");
Ok((container, url))
}
fn spawn_publisher(
url: &str,
channel: &str,
payload: &[u8],
) -> (Arc<AtomicBool>, std::thread::JoinHandle<()>) {
let done = Arc::new(AtomicBool::new(false));
let done_thread = done.clone();
let url = url.to_string();
let channel = channel.to_string();
let payload = payload.to_vec();
let handle = std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let client = redis::Client::open(url.as_str()).unwrap();
let mut conn = client.get_multiplexed_async_connection().await.unwrap();
while !done_thread.load(Ordering::Relaxed) {
let _: i64 = redis::cmd("PUBLISH")
.arg(channel.as_str())
.arg(payload.as_slice())
.query_async(&mut conn)
.await
.unwrap_or(0);
tokio::time::sleep(Duration::from_millis(50)).await;
}
});
});
(done, handle)
}
fn spawn_subscriber(
url: &str,
channel: &str,
n: usize,
ready: Arc<AtomicBool>,
) -> std::thread::JoinHandle<anyhow::Result<Vec<Vec<u8>>>> {
let url = url.to_string();
let channel = channel.to_string();
std::thread::spawn(move || -> anyhow::Result<Vec<Vec<u8>>> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let client = redis::Client::open(url.as_str())?;
let mut pubsub = client.get_async_pubsub().await?;
pubsub.subscribe(channel.as_str()).await?;
ready.store(true, Ordering::Relaxed);
let mut messages = pubsub.on_message();
let mut out = Vec::new();
while out.len() < n {
match tokio::time::timeout(Duration::from_secs(10), messages.next()).await {
Ok(Some(msg)) => out.push(msg.get_payload_bytes().to_vec()),
_ => break,
}
}
Ok(out)
})
})
}
#[test]
fn test_connection_refused() {
let conn = RedisConnection::new("redis://127.0.0.1:59999");
let result = redis_sub(conn, "ch")
.collapse()
.collect()
.run(RunMode::RealTime, RunFor::Cycles(1));
assert!(result.is_err(), "expected connection error");
}
#[test]
fn test_sub_live_message() -> anyhow::Result<()> {
let (_container, url) = start_redis()?;
let conn = RedisConnection::new(&url);
let (done, publisher) = spawn_publisher(&url, "live", b"hello");
let collected = redis_sub(conn, "live").collapse().collect();
collected
.clone()
.run(RunMode::RealTime, RunFor::Cycles(1))?;
done.store(true, Ordering::Relaxed);
publisher.join().unwrap();
let events = collected.peek_value();
assert_eq!(events.len(), 1);
assert_eq!(events[0].value.channel, "live");
assert_eq!(events[0].value.payload, b"hello");
Ok(())
}
#[test]
fn test_pub_round_trip() -> anyhow::Result<()> {
let (_container, url) = start_redis()?;
let conn = RedisConnection::new(&url);
let ready = Arc::new(AtomicBool::new(false));
let subscriber = spawn_subscriber(&url, "rt", 1, ready.clone());
while !ready.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(20));
}
let source = constant(burst![RedisEntry {
channel: "rt".to_string(),
payload: b"value1".to_vec(),
}]);
redis_pub(conn, &source).run(RunMode::RealTime, RunFor::Cycles(1))?;
let payloads = subscriber.join().unwrap()?;
assert_eq!(payloads, vec![b"value1".to_vec()]);
Ok(())
}
#[test]
fn test_pub_multiple_entries_in_burst() -> anyhow::Result<()> {
let (_container, url) = start_redis()?;
let conn = RedisConnection::new(&url);
let ready = Arc::new(AtomicBool::new(false));
let subscriber = spawn_subscriber(&url, "multi", 3, ready.clone());
while !ready.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(20));
}
let source: Rc<dyn crate::Stream<Burst<RedisEntry>>> = constant(burst![
RedisEntry {
channel: "multi".to_string(),
payload: b"a".to_vec()
},
RedisEntry {
channel: "multi".to_string(),
payload: b"b".to_vec()
},
RedisEntry {
channel: "multi".to_string(),
payload: b"c".to_vec()
},
]);
redis_pub(conn, &source).run(RunMode::RealTime, RunFor::Cycles(1))?;
let mut payloads = subscriber.join().unwrap()?;
payloads.sort();
assert_eq!(payloads, vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()]);
Ok(())
}
#[test]
fn test_redis_entry_payload_str() {
let entry = RedisEntry {
channel: "ch".to_string(),
payload: b"bar".to_vec(),
};
assert_eq!(entry.payload_str().unwrap(), "bar");
}
fn xadd(url: &str, key: &str, field: &str, value: &[u8]) -> anyhow::Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let client = redis::Client::open(url)?;
let mut conn = client.get_multiplexed_async_connection().await?;
redis::cmd("XADD")
.arg(key)
.arg("*")
.arg(field)
.arg(value)
.query_async::<String>(&mut conn)
.await?;
Ok(())
})
}
#[test]
fn test_stream_write_and_read_snapshot() -> anyhow::Result<()> {
let (_container, url) = start_redis()?;
let conn = RedisConnection::new(&url);
let source: Rc<dyn crate::Stream<Burst<RedisStreamRecord>>> = constant(burst![
RedisStreamRecord::single("events", "kind", b"login".to_vec()),
RedisStreamRecord::single("events", "kind", b"logout".to_vec()),
]);
redis_stream_write(conn.clone(), &source).run(RunMode::RealTime, RunFor::Cycles(1))?;
let collected = redis_stream_read(conn, "events").collect();
collected
.clone()
.run(RunMode::RealTime, RunFor::Cycles(1))?;
let events: Vec<RedisStreamEvent> = collected
.peek_value()
.into_iter()
.flat_map(|v| v.value.into_iter())
.collect();
assert_eq!(events.len(), 2);
let kinds: Vec<Vec<u8>> = events
.iter()
.map(|e| e.field("kind").unwrap_or_default().to_vec())
.collect();
assert_eq!(kinds, vec![b"login".to_vec(), b"logout".to_vec()]);
assert!(events[0].id < events[1].id);
Ok(())
}
#[test]
fn test_stream_read_live_tail() -> anyhow::Result<()> {
let (_container, url) = start_redis()?;
let conn = RedisConnection::new(&url);
let url_writer = url.clone();
let writer = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(300));
xadd(&url_writer, "tail", "msg", b"live").unwrap();
});
let collected = redis_stream_read(conn, "tail").collapse().collect();
collected
.clone()
.run(RunMode::RealTime, RunFor::Cycles(1))?;
writer.join().unwrap();
let events = collected.peek_value();
assert_eq!(events.len(), 1);
assert_eq!(events[0].value.key, "tail");
assert_eq!(events[0].value.field("msg"), Some(b"live".as_ref()));
Ok(())
}
#[test]
fn test_stream_record_single() {
let record = RedisStreamRecord::single("k", "f", b"v".to_vec());
assert_eq!(record.key, "k");
assert_eq!(record.fields, vec![("f".to_string(), b"v".to_vec())]);
}