use std::sync::Arc;
use aok::OK;
use compio::runtime::Runtime;
use tempfile::{TempDir, tempdir};
use wdev::SegmentedDevice;
use wedb_standalone::{
lua::{
lua_commands::{LuaCommands, LuaSessionContext, StoreScriptCache},
lua_options::{LuaLoggingMode, LuaOptions},
lua_runner::{LuaRunner, RespObject},
scripting_api::ScriptingApi,
session_script_cache::SessionScriptCache,
storage_scripting_api::StorageScriptingApi,
},
storage::session::storage_session::StorageSession,
};
use wkv::{StoreConfig, WedbStore};
type TestStore = Arc<WedbStore<SegmentedDevice>>;
fn open_store(tag: &str) -> aok::Result<(TempDir, TestStore)> {
let dir = tempdir()?;
let device = Arc::new(SegmentedDevice::single_file(dir.path().join(tag))?);
let config = StoreConfig::new(16384, 65536, 64, 0.5)?;
let store = Arc::new(WedbStore::open(config, device)?);
Ok((dir, store))
}
fn eval_args(script: &str, keys: &[&[u8]], argv: &[&[u8]]) -> Vec<Vec<u8>> {
let mut args = vec![
script.as_bytes().to_vec(),
keys.len().to_string().into_bytes(),
];
args.extend(keys.iter().map(|k| k.to_vec()));
args.extend(argv.iter().map(|a| a.to_vec()));
args
}
fn evalsha_args(digest: &str, keys: &[&[u8]], argv: &[&[u8]]) -> Vec<Vec<u8>> {
let mut args = vec![
digest.as_bytes().to_vec(),
keys.len().to_string().into_bytes(),
];
args.extend(keys.iter().map(|k| k.to_vec()));
args.extend(argv.iter().map(|a| a.to_vec()));
args
}
struct Harness<'a> {
cache: &'a mut SessionScriptCache,
store: &'a StoreScriptCache,
api: &'a mut dyn ScriptingApi,
options: &'a LuaOptions,
}
impl<'a> Harness<'a> {
fn with<F>(&mut self, args: &[Vec<u8>], out: &mut Vec<u8>, entry: F) -> bool
where
F: FnOnce(&mut LuaSessionContext) -> bool,
{
out.clear();
let mut ctx = LuaSessionContext {
args,
out,
session_cache: self.cache,
store_cache: self.store,
session: self.api,
lua_enabled: true,
txn_mode: false,
redis_version: "7.4.0",
lua_options: self.options,
};
entry(&mut ctx)
}
fn eval(&mut self, args: &[Vec<u8>], out: &mut Vec<u8>) -> bool {
self.with(args, out, LuaCommands::try_eval)
}
fn evalsha(&mut self, args: &[Vec<u8>], out: &mut Vec<u8>) -> bool {
self.with(args, out, LuaCommands::try_evalsha)
}
fn script_load(&mut self, args: &[Vec<u8>], out: &mut Vec<u8>) -> bool {
self.with(args, out, LuaCommands::network_script_load)
}
fn script_exists(&mut self, args: &[Vec<u8>], out: &mut Vec<u8>) -> bool {
self.with(args, out, LuaCommands::network_script_exists)
}
fn script_flush(&mut self, args: &[Vec<u8>], out: &mut Vec<u8>) -> bool {
self.with(args, out, LuaCommands::network_script_flush)
}
}
#[test]
fn test_eval_redis_call_roundtrip() -> aok::Void {
let rt = Runtime::new()?;
rt.block_on(async {
let (_dir, store) = open_store("lua-eval.db")?;
let session = store.new_session()?;
let storage = StorageSession::new(session.enter_batch());
let mut api = StorageScriptingApi::new(&storage, None);
let mut cache = SessionScriptCache::default();
let store_cache = StoreScriptCache::default();
let options = LuaOptions::default();
let mut out = Vec::new();
let mut harness = Harness {
cache: &mut cache,
store: &store_cache,
api: &mut api,
options: &options,
};
let args = eval_args(
"return redis.call('SET', KEYS[1], ARGV[1])",
&[b"lua:k"],
&[b"hello-lua"],
);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b"$2\r\nOK\r\n");
let args = eval_args("return redis.call('GET', KEYS[1])", &[b"lua:k"], &[]);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b"$9\r\nhello-lua\r\n");
let args = eval_args("return redis.call('GET', KEYS[1])", &[b"lua:miss"], &[]);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b"$-1\r\n");
let args = eval_args("return redis.call('INCR', KEYS[1])", &[b"lua:cnt"], &[]);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b":1\r\n");
let args = eval_args(
"return redis.call('INCRBY', KEYS[1], 41)",
&[b"lua:cnt"],
&[],
);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b":42\r\n");
let direct = match StorageScriptingApi::get(&mut api, b"lua:k") {
Ok(v) => v,
Err(e) => panic!("直读失败: {e}"),
};
assert_eq!(direct, Some(b"hello-lua".to_vec()));
OK
})
}
#[test]
fn test_script_cache_lifecycle() -> aok::Void {
let rt = Runtime::new()?;
rt.block_on(async {
let (_dir, store) = open_store("lua-script.db")?;
let session = store.new_session()?;
let storage = StorageSession::new(session.enter_batch());
let mut api = StorageScriptingApi::new(&storage, None);
let mut cache = SessionScriptCache::default();
let store_cache = StoreScriptCache::default();
let options = LuaOptions::default();
let mut out = Vec::new();
let mut harness = Harness {
cache: &mut cache,
store: &store_cache,
api: &mut api,
options: &options,
};
let load_args = vec![b"return tonumber(ARGV[1]) + 1".to_vec()];
assert!(harness.script_load(&load_args, &mut out));
assert!(out.starts_with(b"$40\r\n"));
let digest = String::from_utf8_lossy(&out[5..45]).to_string();
let exists_args = vec![digest.clone().into_bytes(), vec![b'0'; 40]];
assert!(harness.script_exists(&exists_args, &mut out));
assert_eq!(out, b"*2\r\n:1\r\n:0\r\n");
let args = evalsha_args(&digest, &[], &[b"41"]);
assert!(harness.evalsha(&args, &mut out));
assert_eq!(out, b":42\r\n");
let fake = "f".repeat(40);
let args = evalsha_args(&fake, &[], &[]);
assert!(harness.evalsha(&args, &mut out));
assert_eq!(out, b"-NOSCRIPT No matching script. Please use EVAL.\r\n");
let args = evalsha_args("short", &[], &[]);
assert!(harness.evalsha(&args, &mut out));
assert!(out.starts_with(b"-NOSCRIPT"));
let flush_args = vec![b"ASYNC".to_vec()];
assert!(harness.script_flush(&flush_args, &mut out));
assert_eq!(out, b"+OK\r\n");
let exists_args = vec![digest.into_bytes()];
assert!(harness.script_exists(&exists_args, &mut out));
assert_eq!(out, b"*1\r\n:0\r\n");
let bad_args = vec![b"NOW".to_vec()];
assert!(harness.script_flush(&bad_args, &mut out));
assert!(out.starts_with(b"-ERR SCRIPT FLUSH"));
OK
})
}
#[test]
fn test_eval_cjson_and_semantics() -> aok::Void {
let rt = Runtime::new()?;
rt.block_on(async {
let (_dir, store) = open_store("lua-cjson.db")?;
let session = store.new_session()?;
let storage = StorageSession::new(session.enter_batch());
let mut api = StorageScriptingApi::new(&storage, None);
let mut cache = SessionScriptCache::default();
let store_cache = StoreScriptCache::default();
let options = LuaOptions::default();
let mut out = Vec::new();
let mut harness = Harness {
cache: &mut cache,
store: &store_cache,
api: &mut api,
options: &options,
};
let args = eval_args("return cjson.encode({a = 'x', b = 3})", &[], &[]);
assert!(harness.eval(&args, &mut out));
let body = String::from_utf8_lossy(&out).to_string();
assert!(body.contains("\"a\":\"x\""), "got {body}");
assert!(body.contains("\"b\":3"), "got {body}");
let args = eval_args(
"local t = cjson.decode(ARGV[1]) return t.k",
&[],
&[br#"{"k":"zz"}"#],
);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b"$2\r\nzz\r\n");
let args = eval_args("return redis.sha1hex('')", &[], &[]);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b"$40\r\nda39a3ee5e6b4b0d3255bfef95601890afd80709\r\n");
let args = eval_args("return 1, 2", &[], &[]);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b":1\r\n");
let args = eval_args("return nil", &[], &[]);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b"$-1\r\n");
let args = eval_args("error('boom')", &[], &[]);
assert!(harness.eval(&args, &mut out));
assert!(
out.starts_with(b"-ERR Lua encountered an error: "),
"got {out:?}"
);
let args = eval_args("return ]]", &[], &[]);
assert!(harness.eval(&args, &mut out));
assert!(out.starts_with(b"-Compilation error: "), "got {out:?}");
let bad_arity = vec![b"return 1".to_vec()];
assert!(harness.eval(&bad_arity, &mut out));
assert_eq!(
out,
b"-ERR wrong number of arguments for 'EVAL' command\r\n"
);
let bad_numkeys = vec![b"return 1".to_vec(), b"nan".to_vec()];
assert!(harness.eval(&bad_numkeys, &mut out));
assert_eq!(out, b"-ERR value is not an integer or out of range.\r\n");
OK
})
}
#[test]
fn test_lua_disabled_error() -> aok::Void {
let rt = Runtime::new()?;
rt.block_on(async {
let (_dir, store) = open_store("lua-disabled.db")?;
let session = store.new_session()?;
let storage = StorageSession::new(session.enter_batch());
let mut api = StorageScriptingApi::new(&storage, None);
let mut cache = SessionScriptCache::default();
let store_cache = StoreScriptCache::default();
let options = LuaOptions::default();
let mut out = Vec::new();
let args = eval_args("return 1", &[], &[]);
let mut ctx = LuaSessionContext {
args: &args,
out: &mut out,
session_cache: &mut cache,
store_cache: &store_cache,
session: &mut api,
lua_enabled: false,
txn_mode: false,
redis_version: "7.4.0",
lua_options: &options,
};
assert!(!LuaCommands::check_lua_enabled(&mut ctx));
assert_eq!(
ctx.out,
b"-ERR This instance has Lua scripting support disabled\r\n"
);
let disabled_len = ctx.out.len();
ctx.out.clear();
assert!(LuaCommands::try_eval(&mut ctx));
assert_eq!(ctx.out.len(), disabled_len);
OK
})
}
#[test]
fn test_eval_acl_denied() -> aok::Void {
let rt = Runtime::new()?;
rt.block_on(async {
let (_dir, store) = open_store("lua-acl.db")?;
let session = store.new_session()?;
let storage = StorageSession::new(session.enter_batch());
let mut api = StorageScriptingApi::new(&storage, Some(vec!["GET".into()]));
let mut cache = SessionScriptCache::default();
let store_cache = StoreScriptCache::default();
let options = LuaOptions::default();
let mut out = Vec::new();
let mut harness = Harness {
cache: &mut cache,
store: &store_cache,
api: &mut api,
options: &options,
};
let args = eval_args("return redis.call('SET', KEYS[1], 'v')", &[b"k"], &[]);
assert!(harness.eval(&args, &mut out));
assert!(
out.starts_with(b"-ERR ") && out.windows(6).any(|w| w == b"NOPERM"),
"got {}",
String::from_utf8_lossy(&out)
);
let args = eval_args("return redis.call('GET', KEYS[1])", &[b"k"], &[]);
assert!(harness.eval(&args, &mut out));
assert_eq!(out, b"$-1\r\n");
OK
})
}
#[test]
fn test_eval_no_session() {
let mut runner = LuaRunner::new(
LuaLoggingMode::Silent,
None,
Default::default(),
b"return redis.call('GET', 'k')".to_vec(),
false,
"0.0.0.0",
)
.unwrap();
let mut out = Vec::new();
runner.compile_for_runner(&mut out).unwrap();
let ret = runner.run_for_runner(None, None).unwrap();
assert_eq!(ret, RespObject::Null);
assert!(!runner.needs_dispose());
}