use rong::*;
mod redis;
pub use redis::*;
pub fn init(ctx: &JSContext) -> JSResult<()> {
ctx.register_hidden_class::<RedisClient>()?;
ctx.register_hidden_class::<RedisSubscription>()?;
let ctor = Class::lookup::<RedisClient>(ctx)?.clone();
ctx.host_namespace().set("RedisClient", ctor)?;
ctx.eval::<()>(Source::from_bytes(
r#"(function() {
const proto = Rong.RedisClient.prototype;
const _subscribe = proto.subscribe;
proto.subscribe = function subscribe(channel, options) {
return options === undefined
? _subscribe.call(this, channel)
: _subscribe.call(this, channel, options);
};
})();"#,
))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rong_test::*;
use std::time::Duration;
use tokio::io::AsyncReadExt;
async fn read_child_stderr(child: &mut tokio::process::Child) -> String {
let Some(mut stderr) = child.stderr.take() else {
return String::new();
};
let mut buf = Vec::new();
if stderr.read_to_end(&mut buf).await.is_err() {
return String::new();
}
String::from_utf8_lossy(&buf).trim().to_string()
}
async fn start_test_redis() -> Result<(String, tokio::process::Child), String> {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.map_err(|e| format!("cannot bind: {}", e))?;
let port = listener
.local_addr()
.map_err(|e| format!("no local addr: {}", e))?
.port();
drop(listener);
let candidates = [
"redis-server",
"redis-server.exe",
"/usr/local/opt/redis/bin/redis-server",
"/opt/homebrew/opt/redis/bin/redis-server",
"/usr/bin/redis-server",
"/usr/local/bin/redis-server",
];
let bin = candidates.iter().find(|c| {
std::process::Command::new(c)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
});
let bin = match bin {
Some(b) => *b,
None => return Err("redis-server not found".to_string()),
};
let mut child = tokio::process::Command::new(bin)
.args([
"--bind",
"127.0.0.1",
"--port",
&port.to_string(),
"--save",
"",
"--appendonly",
"no",
"--loglevel",
"warning",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|e| format!("failed to spawn redis-server: {}", e))?;
for _ in 0..150 {
if let Some(status) = child
.try_wait()
.map_err(|e| format!("failed to poll redis-server: {}", e))?
{
let stderr = read_child_stderr(&mut child).await;
let details = if stderr.is_empty() {
format!("redis-server exited early with status {}", status)
} else {
format!(
"redis-server exited early with status {}: {}",
status, stderr
)
};
return Err(details);
}
if tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.is_ok()
{
let url = format!("redis://127.0.0.1:{}", port);
return Ok((url, child));
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let _ = child.kill().await;
let _ = child.wait().await;
let stderr = read_child_stderr(&mut child).await;
let details = if stderr.is_empty() {
"redis-server did not start in time".to_string()
} else {
format!("redis-server did not start in time: {}", stderr)
};
Err(details)
}
async fn setup_redis_env(
ctx: &JSContext,
) -> Result<(String, tokio::process::Child), RongJSError> {
let (url, child) = start_test_redis().await.map_err(|msg| {
HostError::new(
"E_TEST_SETUP",
format!("Failed to start redis-server for rong_redis tests: {}", msg),
)
})?;
ctx.global().set("TEST_REDIS_URL", url.as_str())?;
rong_assert::init(ctx)?;
rong_console::init(ctx)?;
rong_abort::init(ctx)?;
rong_timer::init(ctx)?;
init(ctx)?;
Ok((url, child))
}
fn is_missing_redis_server(err: &RongJSError) -> bool {
format!("{err}").contains("redis-server not found")
}
#[test]
fn test_redis() {
async_run!(|ctx: JSContext| async move {
let (_url, _child) = match setup_redis_env(&ctx).await {
Ok(values) => values,
Err(err) if is_missing_redis_server(&err) => return Ok(()),
Err(err) => return Err(err),
};
let passed = UnitJSRunner::load_script(&ctx, "redis.js")
.await?
.run()
.await?;
assert!(passed);
Ok(())
});
}
#[test]
fn test_redis_namespace() {
async_run!(|ctx: JSContext| async move {
let (url, _child) = match setup_redis_env(&ctx).await {
Ok(values) => values,
Err(err) if is_missing_redis_server(&err) => return Ok(()),
Err(err) => return Err(err),
};
let client = RedisClient::new(url, Some("app1:".to_string()));
let js_client = Class::lookup::<RedisClient>(&ctx)?.instance(client);
ctx.global().set("redis", js_client)?;
let passed = UnitJSRunner::load_script(&ctx, "redis_namespace.js")
.await?
.run()
.await?;
assert!(passed);
Ok(())
});
}
}