mod args;
mod arrays;
mod blocking;
mod cpu;
mod hashes;
mod keyspace;
mod lists;
mod scan;
mod scripting;
mod server;
mod sets;
mod strings;
pub mod table;
mod zsets;
pub use args::Args;
pub use blocking::{Parked, Waiters};
pub use table::{COMMANDS, Spec, arity_ok, lookup};
use crate::reply::Out;
use yo_common::{Code, Error};
use yo_kv::{Clock, Keyspace};
pub const DATABASES: usize = 16;
const ALL_DATABASES: u64 = if DATABASES == 64 {
u64::MAX
} else {
(1u64 << DATABASES) - 1
};
const _: () = assert!(DATABASES <= 64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Flow {
Continue,
Close,
Block,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Stats {
pub clients: u64,
pub connections: u64,
pub commands: u64,
}
pub struct Server {
dbs: Vec<Keyspace>,
clock: Clock,
started_ms: u64,
next_db: usize,
dirty: u64,
conn_bytes: usize,
waiters: Waiters,
pub stats: Stats,
}
impl Server {
#[must_use]
pub fn new() -> Server {
let clock = Clock::system();
Server {
dbs: (0..DATABASES)
.map(|_| Keyspace::with_clock(clock))
.collect(),
clock,
started_ms: clock.now_ms(),
next_db: 0,
dirty: ALL_DATABASES,
conn_bytes: 0,
waiters: Waiters::default(),
stats: Stats::default(),
}
}
#[must_use]
pub fn with_clock(clock: Clock) -> Server {
Server {
dbs: (0..DATABASES)
.map(|_| Keyspace::with_clock(clock))
.collect(),
clock,
started_ms: clock.now_ms(),
next_db: 0,
dirty: ALL_DATABASES,
conn_bytes: 0,
waiters: Waiters::default(),
stats: Stats::default(),
}
}
pub fn db(&mut self, i: usize) -> &mut Keyspace {
self.dirty |= 1u64 << i;
&mut self.dbs[i]
}
#[must_use]
pub fn db_ref(&self, i: usize) -> &Keyspace {
&self.dbs[i]
}
pub fn refresh_clock(&mut self) {
self.clock.refresh();
let now = self.clock.now_ms();
for db in &mut self.dbs {
db.clock_mut().set(now);
}
}
pub fn set_clock_ms(&mut self, ms: u64) {
self.clock.set(ms);
for db in &mut self.dbs {
db.clock_mut().set(ms);
}
}
#[must_use]
pub fn uptime_secs(&self) -> u64 {
self.clock.now_ms().saturating_sub(self.started_ms) / 1000
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.dbs.iter().map(Keyspace::memory_bytes).sum::<usize>() + self.conn_bytes
}
#[must_use]
pub fn dataset_bytes(&self) -> usize {
self.dbs
.iter()
.map(|db| db.map().arena().live_bytes() as usize)
.sum()
}
#[must_use]
pub fn arena_bytes(&self) -> usize {
self.dbs
.iter()
.map(|db| db.map().arena().reserved_bytes() as usize)
.sum()
}
#[must_use]
pub fn index_bytes(&self) -> usize {
self.dbs
.iter()
.map(|db| db.map().index().memory_bytes())
.sum()
}
#[must_use]
pub fn segment_count(&self) -> usize {
self.dbs
.iter()
.map(|db| db.map().arena().resident_segments())
.sum()
}
#[must_use]
pub const fn conn_bytes(&self) -> usize {
self.conn_bytes
}
pub fn note_conn_bytes(&mut self, delta: isize) {
self.conn_bytes = self.conn_bytes.saturating_add_signed(delta);
}
#[must_use]
pub fn expired_keys(&self) -> u64 {
self.dbs.iter().map(Keyspace::expired_keys).sum()
}
pub fn compact_step(&mut self) -> Option<usize> {
for turn in 0..self.dbs.len() {
let i = (self.next_db + turn) % self.dbs.len();
if self.dirty & (1 << i) == 0 {
continue;
}
if let Some(moved) = self.dbs[i].compact_step() {
self.next_db = (i + 1) % self.dbs.len();
return Some(moved);
}
self.dirty &= !(1u64 << i);
}
None
}
}
impl Default for Server {
fn default() -> Server {
Server::new()
}
}
pub struct Session {
db: usize,
id: u64,
name: Vec<u8>,
}
impl Session {
#[must_use]
pub fn new(id: u64) -> Session {
Session {
db: 0,
id,
name: Vec::new(),
}
}
#[must_use]
pub const fn id(&self) -> u64 {
self.id
}
#[must_use]
pub const fn db(&self) -> usize {
self.db
}
#[must_use]
pub fn name(&self) -> &[u8] {
&self.name
}
pub fn reset(&mut self) {
self.db = 0;
self.name.clear();
}
fn set_name(&mut self, name: &[u8]) {
yo_alloc::allow(|| {
self.name.clear();
self.name.extend_from_slice(name);
});
}
}
pub fn execute(server: &mut Server, session: &mut Session, args: Args<'_>, out: &mut Out) -> Flow {
if args.is_empty() {
return Flow::Continue;
}
server.stats.commands += 1;
let Some(spec) = lookup(args.name()) else {
write_error(out, &args::unknown_command(args));
return Flow::Continue;
};
if !arity_ok(spec, args.len()) {
write_error(out, &args::wrong_arity(spec.name));
return Flow::Continue;
}
server.dirty |= match spec.group {
"string" | "set" | "hash" | "list" | "zset" | "array" => 1u64 << session.db,
_ => ALL_DATABASES,
};
let mark = out.len();
let done = if spec.flags.contains(&"blocking") {
blocking::execute(server, session, spec, args, out)
} else {
match spec.group {
"string" => {
let db = session.db;
strings::execute(&mut server.dbs[db], spec, args, out).map(|()| Flow::Continue)
}
"set" => {
let db = session.db;
sets::execute(&mut server.dbs[db], spec, args, out).map(|()| Flow::Continue)
}
"hash" => {
let db = session.db;
hashes::execute(&mut server.dbs[db], spec, args, out).map(|()| Flow::Continue)
}
"list" => {
let db = session.db;
lists::execute(&mut server.dbs[db], spec, args, out).map(|()| Flow::Continue)
}
"zset" => {
let db = session.db;
zsets::execute(&mut server.dbs[db], spec, args, out).map(|()| Flow::Continue)
}
"array" => {
let db = session.db;
arrays::execute(&mut server.dbs[db], spec, args, out).map(|()| Flow::Continue)
}
"keyspace" => keyspace::execute(&mut server.dbs, session.db, spec, args, out)
.map(|()| Flow::Continue),
"scripting" => scripting::execute(spec, args, out).map(|()| Flow::Continue),
_ => server::execute(server, session, spec, args, out),
}
};
match done {
Ok(flow) => flow,
Err(e) => {
out.truncate(mark);
write_error(out, &e);
Flow::Continue
}
}
}
fn write_error(out: &mut Out, e: &Error) {
let prefix: &[u8] = match e.code() {
Code::WrongType => b"WRONGTYPE ",
_ => b"ERR ",
};
out.error_line(prefix, e.message().as_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::{Limits, Proto};
use crate::request::Argv;
pub(crate) fn encode(parts: &[&[u8]]) -> Vec<u8> {
let mut wire = format!("*{}\r\n", parts.len()).into_bytes();
for p in parts {
wire.extend_from_slice(format!("${}\r\n", p.len()).as_bytes());
wire.extend_from_slice(p);
wire.extend_from_slice(b"\r\n");
}
wire
}
struct Fixture {
server: Server,
session: Session,
argv: Argv,
out: Out,
}
impl Fixture {
fn new() -> Fixture {
Fixture {
server: Server::new(),
session: Session::new(7),
argv: Argv::new(),
out: Out::new(Proto::Resp2),
}
}
fn run(&mut self, parts: &[&[u8]]) -> String {
self.flow(parts).1
}
fn flow(&mut self, parts: &[&[u8]]) -> (Flow, String) {
let wire = encode(parts);
self.argv.decode(&wire, &Limits::default()).unwrap();
self.out.clear();
let flow = execute(
&mut self.server,
&mut self.session,
Args::new(&self.argv, &wire),
&mut self.out,
);
(
flow,
String::from_utf8_lossy(self.out.as_slice()).into_owned(),
)
}
}
#[test]
fn rewriting_the_same_keys_does_not_grow_the_server() {
let mut f = Fixture::new();
let val = vec![b'v'; 1024];
let keys: Vec<Vec<u8>> = (0..64).map(|i| format!("key:{i}").into_bytes()).collect();
for k in &keys {
f.run(&[b"SET", k, &val]);
}
f.server.compact_step();
let after_first = f.server.memory_bytes();
for _ in 0..500 {
for k in &keys {
f.run(&[b"SET", k, &val]);
}
f.server.compact_step();
}
assert!(
f.server.memory_bytes() <= after_first * 2,
"held {} after five hundred passes against {after_first} after one",
f.server.memory_bytes()
);
assert_eq!(f.run(&[b"DBSIZE"]), format!(":{}\r\n", keys.len()));
assert_eq!(f.run(&[b"STRLEN", b"key:7"]), ":1024\r\n");
}
#[test]
fn a_database_nobody_started_on_is_still_collected() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SELECT", b"9"]), "+OK\r\n");
let val = vec![b'v'; 1024];
let keys: Vec<Vec<u8>> = (0..64).map(|i| format!("key:{i}").into_bytes()).collect();
for k in &keys {
f.run(&[b"SET", k, &val]);
}
while f.server.compact_step().is_some() {}
assert_eq!(
f.server.dirty & (1 << 9),
0,
"database nine was drained and should not be asked again until it is written to"
);
let after_first = f.server.memory_bytes();
for _ in 0..500 {
for k in &keys {
f.run(&[b"SET", k, &val]);
}
f.server.compact_step();
}
assert!(
f.server.memory_bytes() <= after_first * 2,
"held {} after five hundred passes against {after_first} after one",
f.server.memory_bytes()
);
assert_eq!(f.run(&[b"DBSIZE"]), format!(":{}\r\n", keys.len()));
assert_eq!(f.run(&[b"STRLEN", b"key:7"]), ":1024\r\n");
f.run(&[b"SELECT", b"0"]);
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
}
#[test]
fn a_command_goes_from_bytes_to_bytes() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SET", b"k", b"v"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"k"]), "$1\r\nv\r\n");
assert_eq!(f.run(&[b"GET", b"nosuch"]), "$-1\r\n");
assert_eq!(f.run(&[b"STRLEN", b"k"]), ":1\r\n");
assert_eq!(f.run(&[b"set", b"k", b"v2", b"xx"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"k"]), "$2\r\nv2\r\n");
}
#[test]
fn deleting_counts_keys_removed_and_existing_counts_arguments_matched() {
let mut f = Fixture::new();
f.run(&[b"MSET", b"a", b"1", b"b", b"2", b"c", b"3"]);
assert_eq!(f.run(&[b"EXISTS", b"a", b"a", b"nosuch"]), ":2\r\n");
assert_eq!(f.run(&[b"DEL", b"a", b"a", b"nosuch"]), ":1\r\n");
assert_eq!(f.run(&[b"EXISTS", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"UNLINK", b"b", b"c"]), ":2\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
}
#[test]
fn type_is_a_simple_string_and_says_none_for_a_key_that_is_not_there() {
let mut f = Fixture::new();
f.run(&[b"SET", b"k", b"v"]);
assert_eq!(f.run(&[b"TYPE", b"k"]), "+string\r\n");
assert_eq!(f.run(&[b"TYPE", b"nosuch"]), "+none\r\n");
}
#[test]
fn touch_counts_the_way_exists_counts() {
let mut f = Fixture::new();
f.run(&[b"MSET", b"a", b"1", b"b", b"2"]);
assert_eq!(f.run(&[b"TOUCH", b"a", b"b"]), ":2\r\n");
assert_eq!(
f.run(&[b"TOUCH", b"a", b"a"]),
":2\r\n",
"twice counts twice"
);
assert_eq!(f.run(&[b"TOUCH", b"a", b"nosuch"]), ":1\r\n");
assert_eq!(f.run(&[b"TOUCH", b"nosuch"]), ":0\r\n");
}
#[test]
fn a_rename_moves_the_deadline_with_the_value_and_drops_the_one_it_lands_on() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"v1", b"EX", b"100"]);
f.run(&[b"SET", b"b", b"v2", b"EX", b"500"]);
assert_eq!(f.run(&[b"RENAME", b"a", b"b"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"b"]), "$2\r\nv1\r\n");
assert_eq!(
f.run(&[b"TTL", b"b"]),
":100\r\n",
"the source's and not b's"
);
assert_eq!(f.run(&[b"EXISTS", b"a"]), ":0\r\n");
}
#[test]
fn a_rename_with_no_source_is_an_error_and_not_a_zero() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"RENAME", b"a", b"b"]), "-ERR no such key\r\n");
assert_eq!(f.run(&[b"RENAMENX", b"a", b"a"]), "-ERR no such key\r\n");
}
#[test]
fn renamenx_refuses_a_taken_name_including_the_one_it_already_has() {
let mut f = Fixture::new();
f.run(&[b"MSET", b"a", b"v1", b"b", b"v2"]);
assert_eq!(f.run(&[b"RENAMENX", b"a", b"b"]), ":0\r\n");
assert_eq!(f.run(&[b"GET", b"b"]), "$2\r\nv2\r\n");
assert_eq!(f.run(&[b"RENAMENX", b"a", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"RENAME", b"a", b"a"]), "+OK\r\n");
assert_eq!(f.run(&[b"RENAMENX", b"a", b"c"]), ":1\r\n");
assert_eq!(f.run(&[b"GET", b"c"]), "$2\r\nv1\r\n");
}
#[test]
fn renaming_a_set_does_not_touch_a_member() {
let mut f = Fixture::new();
for i in 0..300 {
f.run(&[b"SADD", b"s", format!("m{i}").as_bytes()]);
}
let before = f.server.memory_bytes();
assert_eq!(f.run(&[b"RENAME", b"s", b"t"]), "+OK\r\n");
assert_eq!(f.run(&[b"SCARD", b"t"]), ":300\r\n");
assert_eq!(f.run(&[b"TYPE", b"t"]), "+set\r\n");
assert!(
f.server.memory_bytes().abs_diff(before) < 256,
"the members were copied: {} against {before}",
f.server.memory_bytes()
);
}
#[test]
fn a_copy_is_a_second_value_and_not_a_second_name() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"s", b"m1", b"m2"]);
assert_eq!(f.run(&[b"COPY", b"s", b"t"]), ":1\r\n");
f.run(&[b"SADD", b"t", b"m3"]);
assert_eq!(f.run(&[b"SCARD", b"s"]), ":2\r\n", "the original is intact");
assert_eq!(f.run(&[b"SCARD", b"t"]), ":3\r\n");
}
#[test]
fn every_type_can_be_copied() {
let mut f = Fixture::new();
f.run(&[b"SET", b"str", b"v1"]);
f.run(&[b"SADD", b"set", b"m1"]);
f.run(&[b"HSET", b"hash", b"f", b"v"]);
f.run(&[b"RPUSH", b"list", b"a", b"b"]);
f.run(&[b"ZADD", b"zset", b"1", b"m1"]);
for name in [
&b"str"[..],
&b"set"[..],
&b"hash"[..],
&b"list"[..],
&b"zset"[..],
] {
let dst = [name, b":copy"].concat();
assert_eq!(
f.run(&[b"COPY", name, &dst]),
":1\r\n",
"copying {}",
String::from_utf8_lossy(name)
);
assert_eq!(f.run(&[b"TYPE", name]), f.run(&[b"TYPE", &dst]));
}
assert_eq!(f.run(&[b"LRANGE", b"list:copy", b"0", b"-1"]), {
let mut want = String::from("*2\r\n");
want.push_str("$1\r\na\r\n$1\r\nb\r\n");
want
});
assert_eq!(f.run(&[b"ZSCORE", b"zset:copy", b"m1"]), "$1\r\n1\r\n");
f.run(&[b"RPUSH", b"list:copy", b"c"]);
assert_eq!(f.run(&[b"LLEN", b"list"]), ":2\r\n");
assert_eq!(f.run(&[b"LLEN", b"list:copy"]), ":3\r\n");
}
#[test]
fn a_copy_refuses_a_taken_destination_until_it_is_told_it_can_have_it() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"v1", b"EX", b"100"]);
f.run(&[b"SET", b"b", b"v2"]);
assert_eq!(f.run(&[b"COPY", b"a", b"b"]), ":0\r\n");
assert_eq!(f.run(&[b"GET", b"b"]), "$2\r\nv2\r\n");
assert_eq!(f.run(&[b"COPY", b"a", b"b", b"REPLACE"]), ":1\r\n");
assert_eq!(f.run(&[b"GET", b"b"]), "$2\r\nv1\r\n");
assert_eq!(f.run(&[b"TTL", b"b"]), ":100\r\n", "the deadline came too");
assert_eq!(f.run(&[b"COPY", b"nosuch", b"z"]), ":0\r\n");
}
#[test]
fn a_copy_into_another_database_is_a_copy_and_onto_itself_there_is_too() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"v1"]);
assert_eq!(f.run(&[b"COPY", b"a", b"a", b"DB", b"1"]), ":1\r\n");
f.run(&[b"SELECT", b"1"]);
assert_eq!(f.run(&[b"GET", b"a"]), "$2\r\nv1\r\n");
assert_eq!(
f.run(&[b"COPY", b"a", b"a", b"DB", b"0"]),
":0\r\n",
"taken"
);
assert_eq!(
f.run(&[b"COPY", b"a", b"a", b"DB", b"0", b"REPLACE"]),
":1\r\n"
);
}
#[test]
fn copy_checks_its_options_before_it_looks_for_anything() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"COPY", b"a", b"b", b"DB", b"99"]),
"-ERR DB index is out of range\r\n"
);
assert_eq!(
f.run(&[b"COPY", b"a", b"b", b"DB", b"-1"]),
"-ERR DB index is out of range\r\n"
);
assert_eq!(
f.run(&[b"COPY", b"a", b"b", b"DB", b"x"]),
"-ERR value is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"COPY", b"a", b"b", b"nonsense"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"COPY", b"a", b"a"]),
"-ERR source and destination objects are the same\r\n"
);
assert_eq!(
f.run(&[b"COPY", b"a", b"b", b"dB", b"1", b"rEpLaCe", b"db", b"2"]),
":0\r\n"
);
}
#[test]
fn time_is_two_bulk_strings_and_moves() {
let mut f = Fixture::new();
let first = f.run(&[b"TIME"]);
assert!(first.starts_with("*2\r\n$"), "got {first}");
let parts: Vec<&str> = first.split("\r\n").collect();
let secs: i64 = parts[2].parse().expect("seconds as decimal text");
let micros: i64 = parts[4].parse().expect("microseconds as decimal text");
assert!(secs > 1_700_000_000, "a real wall clock, got {secs}");
assert!((0..1_000_000).contains(µs), "got {micros}");
assert_ne!(first, f.run(&[b"TIME"]));
}
#[test]
fn a_keyspace_scan_walks_every_key_once() {
let mut f = Fixture::new();
for i in 0..500 {
f.run(&[b"SET", format!("k{i}").as_bytes(), b"v"]);
}
let mut seen: Vec<String> = Vec::new();
let mut cursor = "0".to_owned();
let mut calls = 0;
loop {
let (next, keys) = scan_reply(&f.run(&[b"SCAN", cursor.as_bytes(), b"COUNT", b"32"]));
seen.extend(keys);
cursor = next;
calls += 1;
assert!(calls < 10_000, "the cursor is not advancing");
if cursor == "0" {
break;
}
}
seen.sort();
seen.dedup();
assert_eq!(seen.len(), 500, "every key once and only once");
assert!(calls > 1, "500 keys came back in one batch");
}
#[test]
fn a_scan_narrows_by_pattern_and_by_type() {
let mut f = Fixture::new();
f.run(&[b"SET", b"str", b"v"]);
f.run(&[b"SADD", b"members", b"a"]);
f.run(&[b"HSET", b"fields", b"f", b"v"]);
let all = |f: &mut Fixture, args: &[&[u8]]| {
let mut out: Vec<String> = Vec::new();
let mut cursor = "0".to_owned();
loop {
let mut line: Vec<&[u8]> = vec![b"SCAN", cursor.as_bytes()];
line.extend_from_slice(args);
let (next, keys) = scan_reply(&f.run(&line));
out.extend(keys);
cursor = next;
if cursor == "0" {
break;
}
}
out.sort();
out
};
assert_eq!(all(&mut f, &[]), ["fields", "members", "str"]);
assert_eq!(all(&mut f, &[b"MATCH", b"*e*"]), ["fields", "members"]);
assert_eq!(all(&mut f, &[b"TYPE", b"set"]), ["members"]);
assert_eq!(all(&mut f, &[b"TYPE", b"HASH"]), ["fields"]);
assert!(all(&mut f, &[b"TYPE", b"list"]).is_empty());
assert!(all(&mut f, &[b"TYPE", b"banana"]).is_empty());
assert!(all(&mut f, &[b"MATCH", b"str*", b"TYPE", b"set"]).is_empty());
}
#[test]
fn a_scan_says_what_is_wrong_with_it() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SCAN", b"nope"]), "-ERR invalid cursor\r\n");
assert_eq!(f.run(&[b"SCAN", b"-1"]), "-ERR invalid cursor\r\n");
assert_eq!(f.run(&[b"SCAN", b"0", b"MATCH"]), "-ERR syntax error\r\n");
assert_eq!(
f.run(&[b"SCAN", b"0", b"COUNT", b"0"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"SCAN", b"0", b"COUNT", b"x"]),
"-ERR value is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"SCAN", b"0", b"WAT", b"1"]),
"-ERR syntax error\r\n"
);
assert!(f.run(&[b"SCAN", b"18446744073709551615"]).starts_with("*2"));
}
#[test]
fn keys_and_randomkey_look_at_the_whole_database() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"KEYS", b"*"]), "*0\r\n");
assert_eq!(f.run(&[b"RANDOMKEY"]), "$-1\r\n");
for name in ["one", "two", "three"] {
f.run(&[b"SET", name.as_bytes(), b"v"]);
}
assert_eq!(sorted(&f.run(&[b"KEYS", b"*"])), ["one", "three", "two"]);
assert_eq!(sorted(&f.run(&[b"KEYS", b"t*"])), ["three", "two"]);
assert_eq!(f.run(&[b"KEYS", b"nothing"]), "*0\r\n");
for _ in 0..50 {
let got = f.run(&[b"RANDOMKEY"]);
assert!(
["$3\r\none\r\n", "$3\r\ntwo\r\n", "$5\r\nthree\r\n"].contains(&got.as_str()),
"got {got}"
);
}
}
#[test]
fn a_walk_does_not_answer_keys_that_have_expired() {
let mut f = Fixture::new();
f.run(&[b"SET", b"alive", b"v"]);
f.run(&[b"SET", b"dead", b"v", b"PX", b"1"]);
f.server.db(0).clock_mut().advance(2);
assert_eq!(
f.run(&[b"DBSIZE"]),
":2\r\n",
"nothing has collected it yet"
);
assert_eq!(f.run(&[b"KEYS", b"*"]), "*1\r\n$5\r\nalive\r\n");
let (_, keys) = scan_reply(&f.run(&[b"SCAN", b"0", b"COUNT", b"1000"]));
assert_eq!(keys, ["alive"]);
for _ in 0..20 {
assert_eq!(f.run(&[b"RANDOMKEY"]), "$5\r\nalive\r\n");
}
assert_eq!(f.run(&[b"DBSIZE"]), ":1\r\n");
}
#[test]
fn a_key_deadline_goes_on_and_comes_back_in_all_four_units() {
let mut f = Fixture::new();
f.run(&[b"SET", b"k", b"v"]);
assert_eq!(f.run(&[b"TTL", b"k"]), ":-1\r\n", "there and no deadline");
assert_eq!(f.run(&[b"TTL", b"nosuch"]), ":-2\r\n", "not there at all");
assert_eq!(f.run(&[b"EXPIRE", b"k", b"100"]), ":1\r\n");
assert_eq!(f.run(&[b"TTL", b"k"]), ":100\r\n");
let ms = int(&f.run(&[b"PTTL", b"k"]));
assert!((99_000..=100_000).contains(&ms), "got {ms}");
let at = int(&f.run(&[b"EXPIRETIME", b"k"]));
let at_ms = int(&f.run(&[b"PEXPIRETIME", b"k"]));
assert_eq!(at, (at_ms + 500) / 1000);
assert!(at_ms > 1_700_000_000_000, "an absolute moment, got {at_ms}");
assert_eq!(f.run(&[b"PERSIST", b"k"]), ":1\r\n");
assert_eq!(f.run(&[b"TTL", b"k"]), ":-1\r\n");
assert_eq!(
f.run(&[b"PERSIST", b"k"]),
":0\r\n",
"nothing to take off the second time"
);
assert_eq!(f.run(&[b"PERSIST", b"nosuch"]), ":0\r\n");
assert_eq!(
f.run(&[b"GET", b"k"]),
"$1\r\nv\r\n",
"and the value went through all of that untouched"
);
}
#[test]
fn every_type_can_be_given_a_deadline_and_it_is_the_same_deadline() {
let mut f = Fixture::new();
f.run(&[b"SET", b"str", b"v"]);
f.run(&[b"SADD", b"set", b"a", b"b"]);
f.run(&[b"HSET", b"hash", b"f", b"v"]);
for key in [b"str".as_slice(), b"set", b"hash"] {
assert_eq!(f.run(&[b"EXPIRE", key, b"100"]), ":1\r\n");
assert_eq!(f.run(&[b"TTL", key]), ":100\r\n");
}
assert_eq!(f.run(&[b"SCARD", b"set"]), ":2\r\n");
assert_eq!(f.run(&[b"HGET", b"hash", b"f"]), "$1\r\nv\r\n");
assert_eq!(f.run(&[b"GET", b"str"]), "$1\r\nv\r\n");
}
#[test]
fn a_deadline_that_has_already_gone_deletes_the_key_now() {
let mut f = Fixture::new();
for key in [b"a".as_slice(), b"b", b"c", b"d"] {
f.run(&[b"SET", key, b"v"]);
}
assert_eq!(f.run(&[b"EXPIRE", b"a", b"0"]), ":1\r\n");
assert_eq!(f.run(&[b"EXPIRE", b"b", b"-1"]), ":1\r\n");
assert_eq!(f.run(&[b"EXPIREAT", b"c", b"1"]), ":1\r\n");
assert_eq!(f.run(&[b"PEXPIREAT", b"d", b"1"]), ":1\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
assert_eq!(
f.run(&[b"EXPIRE", b"a", b"100"]),
":0\r\n",
"and the key really went, so there is nothing to put a deadline on"
);
}
#[test]
fn the_four_conditions_decide_whether_the_deadline_moves() {
let mut f = Fixture::new();
f.run(&[b"SET", b"k", b"v"]);
assert_eq!(f.run(&[b"EXPIRE", b"k", b"100", b"XX"]), ":0\r\n");
assert_eq!(f.run(&[b"TTL", b"k"]), ":-1\r\n", "and XX left it alone");
assert_eq!(f.run(&[b"EXPIRE", b"k", b"100", b"GT"]), ":0\r\n");
assert_eq!(
f.run(&[b"EXPIRE", b"k", b"100", b"LT"]),
":1\r\n",
"no deadline reads as infinitely far away, so LT passes where GT fails"
);
assert_eq!(f.run(&[b"EXPIRE", b"k", b"50", b"NX"]), ":0\r\n");
assert_eq!(f.run(&[b"EXPIRE", b"k", b"50", b"GT"]), ":0\r\n");
assert_eq!(f.run(&[b"TTL", b"k"]), ":100\r\n");
assert_eq!(f.run(&[b"EXPIRE", b"k", b"50", b"LT"]), ":1\r\n");
assert_eq!(f.run(&[b"EXPIRE", b"k", b"200", b"GT"]), ":1\r\n");
assert_eq!(f.run(&[b"TTL", b"k"]), ":200\r\n");
assert_eq!(f.run(&[b"EXPIRE", b"k", b"0", b"NX"]), ":0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"k"]), ":1\r\n");
assert_eq!(f.run(&[b"EXPIRE", b"k", b"0", b"XX"]), ":1\r\n");
assert_eq!(f.run(&[b"EXISTS", b"k"]), ":0\r\n", "and XX let it through");
}
#[test]
fn the_conditions_are_a_set_and_not_a_keyword() {
let mut f = Fixture::new();
f.run(&[b"SET", b"k", b"v"]);
assert_eq!(f.run(&[b"EXPIRE", b"k", b"100", b"nx"]), ":1\r\n");
assert_eq!(
f.run(&[b"EXPIRE", b"k", b"100", b"nx", b"nx"]),
":0\r\n",
"the same keyword twice means it once, and NX now has a deadline to fail on"
);
assert_eq!(f.run(&[b"EXPIRE", b"k", b"200", b"xx", b"gt"]), ":1\r\n");
assert_eq!(f.run(&[b"TTL", b"k"]), ":200\r\n");
assert_eq!(f.run(&[b"EXPIRE", b"k", b"100", b"gt", b"xx"]), ":0\r\n");
assert_eq!(f.run(&[b"EXPIRE", b"k", b"100", b"XX", b"LT"]), ":1\r\n");
assert_eq!(f.run(&[b"TTL", b"k"]), ":100\r\n");
f.run(&[b"PERSIST", b"k"]);
assert_eq!(
f.run(&[b"EXPIRE", b"k", b"100", b"XX", b"LT"]),
":0\r\n",
"where LT on its own would have taken it"
);
assert_eq!(f.run(&[b"EXPIRE", b"k", b"100", b"LT"]), ":1\r\n");
}
#[test]
fn a_key_is_gone_once_its_moment_passes() {
let mut f = Fixture::new();
f.run(&[b"SET", b"k", b"v"]);
f.run(&[b"EXPIRE", b"k", b"100"]);
let at = int(&f.run(&[b"PEXPIRETIME", b"k"]));
f.server.set_clock_ms(at as u64 + 1);
assert_eq!(f.run(&[b"GET", b"k"]), "$-1\r\n");
assert_eq!(f.run(&[b"TTL", b"k"]), ":-2\r\n");
assert_eq!(f.run(&[b"EXISTS", b"k"]), ":0\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
}
#[test]
fn the_expiry_commands_refuse_what_a_real_server_refuses() {
let mut f = Fixture::new();
f.run(&[b"SET", b"k", b"v"]);
for (bad, want) in [
(
&[b"EXPIRE".as_slice(), b"k", b"soon"][..],
"-ERR value is not an integer or out of range\r\n",
),
(
&[b"EXPIRE", b"k", b"100", b"MAYBE"],
"-ERR Unsupported option MAYBE\r\n",
),
(
&[b"EXPIRE", b"k", b"100", b"NX", b"XX"],
"-ERR NX and XX, GT or LT options at the same time are not compatible\r\n",
),
(
&[b"EXPIRE", b"k", b"100", b"NX", b"GT"],
"-ERR NX and XX, GT or LT options at the same time are not compatible\r\n",
),
(
&[b"EXPIRE", b"k", b"100", b"GT", b"LT", b"GT"],
"-ERR GT and LT options at the same time are not compatible\r\n",
),
(
&[b"EXPIRE", b"k", b"9223372036854775807"],
"-ERR invalid expire time in 'expire' command\r\n",
),
(
&[b"EXPIREAT", b"k", b"9223372036854775807"],
"-ERR invalid expire time in 'expireat' command\r\n",
),
(
&[b"PEXPIRE", b"k", b"9223372036854775807"],
"-ERR invalid expire time in 'pexpire' command\r\n",
),
] {
assert_eq!(f.run(bad), want, "for {bad:?}");
}
assert_eq!(
f.run(&[b"TTL", b"k"]),
":-1\r\n",
"and none of those put a deadline on anything"
);
assert_eq!(
f.run(&[b"PEXPIREAT", b"k", b"9223372036854775807"]),
":1\r\n"
);
assert_eq!(f.run(&[b"PEXPIRETIME", b"k"]), ":70368744177663\r\n");
}
#[test]
fn flushing_empties_this_database_or_every_one_of_them() {
let mut f = Fixture::new();
f.run(&[b"SELECT", b"0"]);
f.run(&[b"MSET", b"a", b"1", b"b", b"2"]);
f.run(&[b"SELECT", b"1"]);
f.run(&[b"SET", b"c", b"3"]);
assert_eq!(f.run(&[b"DBSIZE"]), ":1\r\n");
assert_eq!(f.run(&[b"FLUSHDB", b"async"]), "+OK\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
f.run(&[b"SELECT", b"0"]);
assert_eq!(f.run(&[b"DBSIZE"]), ":2\r\n");
assert_eq!(f.run(&[b"FLUSHALL", b"SYNC"]), "+OK\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
f.run(&[b"SELECT", b"1"]);
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
assert_eq!(f.run(&[b"FLUSHALL", b"nope"]), "-ERR syntax error\r\n");
assert_eq!(
f.run(&[b"FLUSHDB", b"sync", b"sync"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn the_script_cache_and_the_library_set_answer_for_being_empty() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SCRIPT", b"FLUSH"]), "+OK\r\n");
assert_eq!(f.run(&[b"SCRIPT", b"FLUSH", b"async"]), "+OK\r\n");
assert_eq!(f.run(&[b"FUNCTION", b"FLUSH", b"SYNC"]), "+OK\r\n");
assert_eq!(
f.run(&[b"SCRIPT", b"EXISTS", b"aaaa", b"bbbb"]),
"*2\r\n:0\r\n:0\r\n"
);
assert_eq!(f.run(&[b"FUNCTION", b"LIST"]), "*0\r\n");
assert_eq!(
f.run(&[b"FUNCTION", b"LIST", b"LIBRARYNAME", b"x", b"WITHCODE"]),
"*0\r\n"
);
assert_eq!(
f.run(&[b"FUNCTION", b"DELETE", b"nosuch"]),
"-ERR Library not found\r\n"
);
assert_eq!(
f.run(&[b"SCRIPT", b"FLUSH", b"nope"]),
"-ERR SCRIPT FLUSH only support SYNC|ASYNC option\r\n"
);
assert_eq!(
f.run(&[b"FUNCTION", b"FLUSH", b"nope"]),
"-ERR FUNCTION FLUSH only supports SYNC|ASYNC option\r\n"
);
assert_eq!(
f.run(&[b"FUNCTION", b"FLUSH", b"sync", b"sync"]),
"-ERR unknown subcommand or wrong number of arguments for 'flush'. Try FUNCTION HELP.\r\n"
);
assert_eq!(
f.run(&[b"FUNCTION", b"LIST", b"bogus"]),
"-ERR Unknown argument bogus\r\n"
);
assert_eq!(
f.run(&[b"SCRIPT", b"EXISTS"]),
"-ERR wrong number of arguments for 'script|exists' command\r\n"
);
assert_eq!(
f.run(&[b"SCRIPT", b"LOAD", b"return 1"]),
"-ERR unknown subcommand 'LOAD'. Try SCRIPT HELP.\r\n"
);
assert_eq!(
f.run(&[b"FUNCTION", b"STATS"]),
"-ERR unknown subcommand 'STATS'. Try FUNCTION HELP.\r\n"
);
}
#[test]
fn a_counter_is_an_integer_and_not_a_string_of_digits() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"INCR", b"c"]), ":1\r\n");
assert_eq!(f.run(&[b"INCRBY", b"c", b"41"]), ":42\r\n");
assert_eq!(f.run(&[b"DECRBY", b"c", b"2"]), ":40\r\n");
assert_eq!(f.run(&[b"GET", b"c"]), "$2\r\n40\r\n");
assert_eq!(f.run(&[b"INCRBYFLOAT", b"c", b"0.5"]), "$4\r\n40.5\r\n");
f.run(&[b"SET", b"k", b"hello"]);
assert_eq!(
f.run(&[b"INCR", b"k"]),
"-ERR value is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"INCRBYFLOAT", b"c", b"inf"]),
"-ERR increment would produce NaN or Infinity\r\n"
);
}
#[test]
fn the_newer_commands_reply_in_the_shapes_a_real_server_sends() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SET", b"k", b"hello"]), "+OK\r\n");
assert_eq!(f.run(&[b"DIGEST", b"k"]), "$16\r\n9555e8555c62dcfd\r\n");
assert_eq!(f.run(&[b"DIGEST", b"nosuch"]), "$-1\r\n");
assert_eq!(f.run(&[b"MSETEX", b"1", b"a", b"1"]), ":1\r\n");
assert_eq!(f.run(&[b"MSETEX", b"1", b"a", b"2", b"NX"]), ":0\r\n");
assert_eq!(f.run(&[b"GET", b"a"]), "$1\r\n1\r\n");
assert_eq!(f.run(&[b"INCREX", b"n"]), "*2\r\n:1\r\n:1\r\n");
assert_eq!(
f.run(&[b"INCREX", b"n", b"BYINT", b"5", b"UBOUND", b"3"]),
"*2\r\n:1\r\n:0\r\n",
"a refused increment reports the value it left alone and applied nothing"
);
assert_eq!(
f.run(&[
b"INCREX",
b"n",
b"BYINT",
b"5",
b"UBOUND",
b"3",
b"SATURATE"
]),
"*2\r\n:3\r\n:2\r\n"
);
assert_eq!(f.run(&[b"DELEX", b"a", b"IFEQ", b"2"]), ":0\r\n");
assert_eq!(f.run(&[b"DELEX", b"a", b"IFEQ", b"1"]), ":1\r\n");
}
#[test]
fn the_same_answers_come_out_in_resp3_spelling() {
let mut f = Fixture::new();
assert!(f.run(&[b"HELLO", b"3"]).starts_with("%7\r\n"));
assert_eq!(f.run(&[b"GET", b"nosuch"]), "_\r\n");
assert_eq!(
f.run(&[b"INCREX", b"c", b"BYFLOAT", b"1.5"]),
"*2\r\n,1.5\r\n,1.5\r\n"
);
assert_eq!(f.run(&[b"INCRBYFLOAT", b"f", b"2.5"]), "$3\r\n2.5\r\n");
assert_eq!(f.run(&[b"RESET"]), "+RESET\r\n");
assert_eq!(f.run(&[b"GET", b"nosuch"]), "$-1\r\n");
}
#[test]
fn a_command_nobody_has_heard_of_is_an_error_and_not_a_closed_socket() {
let mut f = Fixture::new();
let (flow, reply) = f.flow(&[b"NOPE", b"a", b"b"]);
assert_eq!(flow, Flow::Continue);
assert_eq!(
reply,
"-ERR unknown command 'NOPE', with args beginning with: 'a' 'b' \r\n"
);
let reply = f.run(&[b"NO\r\n+PONG\r\nPE"]);
assert_eq!(reply.matches("\r\n").count(), 1);
}
#[test]
fn arity_is_checked_before_the_command_is() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"GET"]),
"-ERR wrong number of arguments for 'get' command\r\n"
);
assert_eq!(
f.run(&[b"MSET", b"k"]),
"-ERR wrong number of arguments for 'mset' command\r\n"
);
assert_eq!(
f.run(&[b"PING", b"a", b"b"]),
"-ERR wrong number of arguments for 'ping' command\r\n"
);
assert_eq!(f.run(&[b"PING"]), "+PONG\r\n");
assert_eq!(f.run(&[b"PING", b"hi"]), "$2\r\nhi\r\n");
assert_eq!(
f.run(&[b"DELEX", b"k", b"IFEQ"]),
"-ERR wrong number of arguments for 'delex' command\r\n"
);
}
#[test]
fn the_option_combinations_are_the_ones_a_real_server_accepts() {
let mut f = Fixture::new();
let syntax = "-ERR syntax error\r\n";
assert_eq!(f.run(&[b"SET", b"k", b"v", b"NX", b"XX"]), syntax);
assert_eq!(f.run(&[b"SET", b"k", b"v", b"NX", b"IFEQ", b"a"]), syntax);
assert_eq!(
f.run(&[b"SET", b"k", b"v", b"KEEPTTL", b"EX", b"5"]),
syntax
);
assert_eq!(
f.run(&[b"SET", b"k", b"v", b"EX", b"5", b"PX", b"5"]),
syntax
);
assert_eq!(f.run(&[b"SET", b"k", b"v", b"PERSIST"]), syntax);
assert_eq!(
f.run(&[b"SET", b"k", b"v", b"EX", b"5", b"EX", b"100"]),
"+OK\r\n"
);
assert_eq!(f.run(&[b"SET", b"k", b"v", b"XX", b"XX"]), "+OK\r\n");
assert_eq!(f.run(&[b"SET", b"k", b"v", b"GET", b"GET"]), "$1\r\nv\r\n");
assert_eq!(
f.run(&[b"INCREX", b"n", b"BYINT", b"1", b"BYINT", b"2"]),
syntax
);
assert_eq!(
f.run(&[b"INCREX", b"n", b"ENX"]),
"-ERR ENX flag requires an expiration\r\n"
);
assert_eq!(
f.run(&[b"INCREX", b"n", b"UBOUND", b"abc"]),
"-ERR UBOUND is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"INCREX", b"n", b"LBOUND", b"10", b"UBOUND", b"5"]),
"-ERR LBOUND can't be greater than UBOUND\r\n"
);
assert_eq!(
f.run(&[b"LCS", b"a", b"b", b"LEN", b"IDX"]),
"-ERR If you want both the length and indexes, please just use IDX.\r\n"
);
}
#[test]
fn the_expiry_rules_are_redis_own() {
let mut f = Fixture::new();
let bad = "-ERR invalid expire time in 'set' command\r\n";
assert_eq!(f.run(&[b"SET", b"k", b"v", b"EX", b"0"]), bad);
assert_eq!(f.run(&[b"SET", b"k", b"v", b"EX", b"-1"]), bad);
assert_eq!(f.run(&[b"SET", b"k", b"v", b"EXAT", b"0"]), bad);
assert_eq!(
f.run(&[b"SET", b"k", b"v", b"EX", b"9999999999999999"]),
bad
);
assert_eq!(
f.run(&[b"SET", b"k", b"v", b"PX", b"99999999999999999999"]),
"-ERR value is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"SETEX", b"k", b"0", b"v"]),
"-ERR invalid expire time in 'setex' command\r\n"
);
assert_eq!(f.run(&[b"GETEX", b"nosuch", b"EX", b"0"]), "$-1\r\n");
assert_eq!(f.run(&[b"GETEX", b"nosuch", b"EX", b"abc"]), "$-1\r\n");
assert_eq!(
f.run(&[b"GETEX", b"nosuch", b"KEEPTTL"]),
"-ERR syntax error\r\n",
"the option list is still checked before the key is looked up"
);
assert_eq!(f.run(&[b"SET", b"k", b"v"]), "+OK\r\n");
assert_eq!(f.run(&[b"SET", b"k", b"v", b"EXAT", b"1"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"k"]), "$-1\r\n");
}
#[test]
fn mset_takes_its_pairs_from_the_read_buffer() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"MSET", b"a", b"1", b"b", b"2"]), "+OK\r\n");
assert_eq!(
f.run(&[b"MGET", b"a", b"b", b"nosuch"]),
"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$-1\r\n"
);
assert_eq!(f.run(&[b"MSETNX", b"b", b"9", b"c", b"3"]), ":0\r\n");
assert_eq!(f.run(&[b"MSETNX", b"c", b"3", b"d", b"4"]), ":1\r\n");
assert_eq!(
f.run(&[b"MSETEX", b"2", b"e", b"5"]),
"-ERR wrong number of key-value pairs\r\n"
);
assert_eq!(
f.run(&[b"MSETEX", b"0", b"e", b"5"]),
"-ERR invalid numkeys value\r\n"
);
assert_eq!(
f.run(&[b"MSETEX", b"abc", b"e", b"5"]),
"-ERR invalid numkeys value\r\n"
);
}
#[test]
fn lcs_answers_the_length_the_string_and_the_runs() {
let mut f = Fixture::new();
f.run(&[b"MSET", b"a", b"ohmytext", b"b", b"mynewtext"]);
assert_eq!(f.run(&[b"LCS", b"a", b"b"]), "$6\r\nmytext\r\n");
assert_eq!(f.run(&[b"LCS", b"a", b"b", b"LEN"]), ":6\r\n");
assert_eq!(
f.run(&[b"LCS", b"a", b"b", b"IDX", b"MINMATCHLEN", b"4"]),
"*4\r\n$7\r\nmatches\r\n*1\r\n*2\r\n*2\r\n:4\r\n:7\r\n*2\r\n:5\r\n:8\r\n$3\r\nlen\r\n:6\r\n"
);
assert_eq!(
f.run(&[b"LCS", b"a", b"b", b"MINMATCHLEN", b"4", b"WITHMATCHLEN"]),
"$6\r\nmytext\r\n"
);
}
#[test]
fn select_moves_the_connection_and_the_databases_stay_apart() {
let mut f = Fixture::new();
f.run(&[b"SET", b"k", b"zero"]);
assert_eq!(f.run(&[b"SELECT", b"4"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"k"]), "$-1\r\n");
f.run(&[b"SET", b"k", b"four"]);
assert_eq!(f.run(&[b"SELECT", b"0"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"k"]), "$4\r\nzero\r\n");
assert_eq!(
f.run(&[b"SELECT", b"99"]),
"-ERR DB index is out of range\r\n"
);
assert_eq!(
f.run(&[b"SELECT", b"-1"]),
"-ERR DB index is out of range\r\n"
);
assert_eq!(
f.run(&[b"SELECT", b"abc"]),
"-ERR value is not an integer or out of range\r\n"
);
f.run(&[b"SELECT", b"4"]);
f.run(&[b"RESET"]);
assert_eq!(f.run(&[b"GET", b"k"]), "$4\r\nzero\r\n");
}
#[test]
fn hello_agrees_on_a_protocol_and_refuses_the_ones_that_do_not_exist() {
let mut f = Fixture::new();
let reply = f.run(&[b"HELLO"]);
assert!(reply.starts_with("*14\r\n"), "{reply}");
assert!(reply.contains("$5\r\nredis\r\n"), "{reply}");
assert!(reply.contains("$5\r\n8.8.0\r\n"), "{reply}");
assert!(
reply.contains(":7\r\n"),
"the connection id is in there: {reply}"
);
assert_eq!(
f.run(&[b"HELLO", b"4"]),
"-NOPROTO unsupported protocol version\r\n"
);
assert_eq!(
f.run(&[b"HELLO", b"abc"]),
"-ERR Protocol version is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"HELLO", b"3", b"SETNAME"]),
"-ERR Syntax error in HELLO option 'SETNAME'\r\n"
);
assert!(
f.run(&[b"HELLO", b"3", b"SETNAME", b"bob"])
.starts_with("%7\r\n")
);
assert_eq!(f.session.name(), b"bob");
f.run(&[b"RESET"]);
assert_eq!(f.session.name(), b"");
}
#[test]
fn command_describes_this_server_in_the_shape_a_driver_reads() {
let mut f = Fixture::new();
let count = format!(":{}\r\n", COMMANDS.len());
assert_eq!(f.run(&[b"COMMAND", b"COUNT"]), count);
let info = f.run(&[b"COMMAND", b"INFO", b"get"]);
assert_eq!(
info,
"*1\r\n*10\r\n$3\r\nget\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n\
*3\r\n+@read\r\n+@string\r\n+@fast\r\n*0\r\n*0\r\n*0\r\n"
);
assert_eq!(f.run(&[b"COMMAND", b"INFO", b"nosuch"]), "*1\r\n$-1\r\n");
assert_eq!(
f.run(&[b"COMMAND", b"LIST", b"FILTERBY", b"PATTERN", b"getr*"]),
"*1\r\n$8\r\ngetrange\r\n"
);
assert_eq!(
f.run(&[b"COMMAND", b"NOPE"]),
"-ERR unknown subcommand 'NOPE'. Try COMMAND HELP.\r\n"
);
}
#[test]
fn command_getkeys_finds_the_keys_including_the_hidden_ones() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"COMMAND", b"GETKEYS", b"get", b"k"]),
"*1\r\n$1\r\nk\r\n"
);
assert_eq!(
f.run(&[b"COMMAND", b"GETKEYS", b"mset", b"a", b"1", b"b", b"2"]),
"*2\r\n$1\r\na\r\n$1\r\nb\r\n"
);
assert_eq!(
f.run(&[
b"COMMAND", b"GETKEYS", b"msetex", b"2", b"a", b"1", b"b", b"2"
]),
"*2\r\n$1\r\na\r\n$1\r\nb\r\n"
);
assert_eq!(
f.run(&[b"COMMAND", b"GETKEYS", b"ping"]),
"-ERR The command has no key arguments\r\n"
);
assert_eq!(
f.run(&[b"COMMAND", b"GETKEYS", b"set"]),
"-ERR Invalid number of arguments specified for command\r\n"
);
}
#[test]
fn config_answers_what_it_can_and_refuses_what_it_cannot() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"maxmemory"]),
"*2\r\n$9\r\nmaxmemory\r\n$1\r\n0\r\n"
);
let both = f.run(&[b"CONFIG", b"GET", b"maxmemory*", b"maxmemory"]);
assert!(both.starts_with("*4\r\n"), "{both}");
assert_eq!(f.run(&[b"CONFIG", b"GET", b"nosuch"]), "*0\r\n");
assert_eq!(f.run(&[b"CONFIG", b"SET", b"appendonly", b"no"]), "+OK\r\n");
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"appendonly", b"yes"]),
"-ERR CONFIG SET failed (possibly related to argument 'appendonly') - can't set immutable config\r\n"
);
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"nosuch", b"1"]),
"-ERR Unknown option or number of arguments for CONFIG SET - 'nosuch'\r\n"
);
assert_eq!(
f.run(&[b"CONFIG", b"GET"]),
"-ERR wrong number of arguments for 'config|get' command\r\n"
);
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"appendonly"]),
"-ERR wrong number of arguments for 'config|set' command\r\n"
);
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"appendonly", b"no", b"maxmemory"]),
"-ERR syntax error\r\n"
);
assert_eq!(f.run(&[b"CONFIG", b"RESETSTAT"]), "+OK\r\n");
assert_eq!(
f.run(&[b"CONFIG", b"REWRITE"]),
"-ERR The server is running without a config file\r\n"
);
}
#[test]
fn object_says_which_rung_of_the_ladder_a_key_is_on() {
let mut f = Fixture::new();
f.run(&[b"SET", b"s", b"hello"]);
f.run(&[b"SET", b"n", b"123"]);
f.run(&[b"SADD", b"si", b"1", b"2", b"3"]);
f.run(&[b"SADD", b"ss", b"a", b"b"]);
f.run(&[b"HSET", b"h", b"f", b"v"]);
for (key, want) in [
(b"s".as_slice(), "embstr"),
(b"n", "int"),
(b"si", "intset"),
(b"ss", "listpack"),
(b"h", "listpack"),
] {
let reply = f.run(&[b"OBJECT", b"ENCODING", key]);
assert_eq!(reply, format!("${}\r\n{want}\r\n", want.len()));
}
f.run(&[b"HEXPIRE", b"h", b"100", b"FIELDS", b"1", b"f"]);
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING", b"h"]),
"$10\r\nlistpackex\r\n"
);
assert_eq!(f.run(&[b"OBJECT", b"REFCOUNT", b"s"]), ":1\r\n");
assert_eq!(f.run(&[b"OBJECT", b"IDLETIME", b"s"]), ":0\r\n");
assert!(f.run(&[b"OBJECT", b"HELP"]).starts_with("*14\r\n+OBJECT "));
}
#[test]
fn object_answers_nil_for_a_key_that_is_not_there() {
let mut f = Fixture::new();
for sub in [b"ENCODING".as_slice(), b"REFCOUNT", b"IDLETIME", b"FREQ"] {
assert_eq!(
f.run(&[b"OBJECT", sub, b"nokey"]),
"$-1\r\n",
"a nil and not an error, which is what 8.10.1 does"
);
}
f.run(&[b"SET", b"s", b"v"]);
assert!(
f.run(&[b"OBJECT", b"FREQ", b"s"])
.starts_with("-ERR An LFU maxmemory policy is not"),
);
assert_eq!(
f.run(&[b"OBJECT", b"NOPE", b"s"]),
"-ERR unknown subcommand 'NOPE'. Try OBJECT HELP.\r\n"
);
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING"]),
"-ERR wrong number of arguments for 'object|encoding' command\r\n"
);
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING", b"s", b"extra"]),
"-ERR wrong number of arguments for 'object|encoding' command\r\n"
);
assert_eq!(
f.run(&[b"OBJECT"]),
"-ERR wrong number of arguments for 'object' command\r\n"
);
}
#[test]
fn config_moves_the_ladder_and_object_encoding_agrees() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"hash-max-listpack-entries"]),
"*2\r\n$25\r\nhash-max-listpack-entries\r\n$3\r\n512\r\n",
"512 and not the 128 everyone remembers, which is what 8.10.1 says"
);
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"hash-max-ziplist-entries"]),
"*2\r\n$24\r\nhash-max-ziplist-entries\r\n$3\r\n512\r\n"
);
assert!(
f.run(&[b"CONFIG", b"GET", b"hash-max-*"])
.starts_with("*8\r\n")
);
assert!(
f.run(&[b"CONFIG", b"GET", b"set-max-*"])
.starts_with("*6\r\n")
);
f.run(&[b"HSET", b"h", b"a", b"1", b"b", b"2", b"c", b"3"]);
assert_eq!(f.run(&[b"OBJECT", b"ENCODING", b"h"]), "$8\r\nlistpack\r\n");
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"hash-max-ziplist-entries", b"2"]),
"+OK\r\n",
"written under the old name and read back under the new one"
);
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"hash-max-listpack-entries"]),
"*2\r\n$25\r\nhash-max-listpack-entries\r\n$1\r\n2\r\n"
);
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING", b"h"]),
"$8\r\nlistpack\r\n",
"the hash that already exists is left exactly where it was"
);
f.run(&[b"HSET", b"h2", b"a", b"1", b"b", b"2", b"c", b"3"]);
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING", b"h2"]),
"$9\r\nhashtable\r\n",
"and the next one built goes straight to a table"
);
f.run(&[b"CONFIG", b"SET", b"set-max-intset-entries", b"2"]);
f.run(&[b"SADD", b"s", b"1", b"2", b"3"]);
assert_eq!(f.run(&[b"OBJECT", b"ENCODING", b"s"]), "$8\r\nlistpack\r\n");
f.run(&[b"CONFIG", b"SET", b"set-max-listpack-value", b"2"]);
f.run(&[b"SADD", b"s2", b"abcdefgh"]);
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING", b"s2"]),
"$9\r\nhashtable\r\n"
);
}
#[test]
fn config_set_takes_all_of_the_ladder_or_none_of_it() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[
b"CONFIG",
b"SET",
b"hash-max-listpack-entries",
b"7",
b"set-max-listpack-entries",
b"abc"
]),
"-ERR CONFIG SET failed (possibly related to argument 'set-max-listpack-entries') - argument couldn't be parsed into an integer\r\n"
);
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"hash-max-listpack-entries"]),
"*2\r\n$25\r\nhash-max-listpack-entries\r\n$3\r\n512\r\n",
"the pair in front of the bad one did not go in"
);
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"hash-max-ziplist-entries", b"abc"]),
"-ERR CONFIG SET failed (possibly related to argument 'hash-max-ziplist-entries') - argument couldn't be parsed into an integer\r\n"
);
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"set-max-intset-entries", b"-1"]),
"-ERR CONFIG SET failed (possibly related to argument 'set-max-intset-entries') - argument must be between 0 and 9223372036854775807 inclusive\r\n"
);
assert_eq!(
f.run(&[
b"CONFIG",
b"SET",
b"set-max-intset-entries",
b"99999999999999999999"
]),
"-ERR CONFIG SET failed (possibly related to argument 'set-max-intset-entries') - argument couldn't be parsed into an integer\r\n"
);
assert_eq!(
f.run(&[
b"CONFIG",
b"SET",
b"set-max-intset-entries",
b"9223372036854775807"
]),
"+OK\r\n"
);
}
#[test]
fn a_setting_moved_on_one_database_moved_on_all_of_them() {
let mut f = Fixture::new();
f.run(&[b"CONFIG", b"SET", b"hash-max-listpack-entries", b"1"]);
f.run(&[b"SELECT", b"3"]);
f.run(&[b"HSET", b"h", b"a", b"1", b"b", b"2"]);
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING", b"h"]),
"$9\r\nhashtable\r\n",
"these are one server wide number in Redis, whatever a Keyspace carries"
);
}
#[test]
fn info_reports_the_numbers_it_can_stand_behind() {
let mut f = Fixture::new();
f.run(&[b"MSET", b"a", b"1", b"b", b"2"]);
let all = f.run(&[b"INFO"]);
assert!(all.contains("redis_version:8.8.0"), "{all}");
assert!(
all.contains(concat!("yo_version:", env!("CARGO_PKG_VERSION"))),
"{all}"
);
assert!(all.contains("db0:keys=2,expires=0,avg_ttl=0"), "{all}");
assert!(all.contains("role:master"), "{all}");
let clients = f.run(&[b"INFO", b"clients"]);
assert!(clients.contains("connected_clients:0"), "{clients}");
assert!(!clients.contains("redis_version"), "{clients}");
assert_eq!(f.run(&[b"INFO", b"nosuch"]), "$0\r\n\r\n");
}
#[cfg(unix)]
#[test]
fn info_cpu_reports_processor_time_that_was_really_measured() {
let mut f = Fixture::new();
let cpu = f.run(&[b"INFO", b"cpu"]);
assert!(cpu.contains("# CPU"), "{cpu}");
assert!(cpu.contains("used_cpu_user:"), "{cpu}");
assert!(cpu.contains("used_cpu_sys:"), "{cpu}");
assert!(cpu.contains("used_cpu_user_children:0.000000"), "{cpu}");
assert!(!cpu.contains("redis_version"), "{cpu}");
let before = used_cpu_user(&cpu);
let mut n = 0u64;
let mut rounds = 0;
while used_cpu_user(&f.run(&[b"INFO", b"cpu"])) <= before {
for i in 0..1_000_000u64 {
n = n.wrapping_add(i.wrapping_mul(i));
}
rounds += 1;
assert!(rounds < 1_000, "cpu time never moved, n is {n}");
}
}
#[cfg(unix)]
fn used_cpu_user(info: &str) -> f64 {
info.lines()
.find_map(|l| l.strip_prefix("used_cpu_user:"))
.expect("no used_cpu_user in the reply")
.trim()
.parse()
.expect("used_cpu_user is not a number")
}
#[test]
fn a_command_that_fails_leaves_nothing_half_written() {
let mut f = Fixture::new();
let reply = f.run(&[b"SETRANGE", b"k", b"-1", b"x"]);
assert_eq!(reply, "-ERR offset is out of range\r\n");
assert!(!reply.contains(':'), "no integer went out in front of it");
}
#[test]
fn quit_answers_first_and_closes_after() {
let mut f = Fixture::new();
let (flow, reply) = f.flow(&[b"QUIT"]);
assert_eq!(reply, "+OK\r\n");
assert_eq!(flow, Flow::Close);
}
#[test]
fn the_command_counter_counts_every_command_including_the_bad_ones() {
let mut f = Fixture::new();
f.run(&[b"PING"]);
f.run(&[b"NOPE"]);
f.run(&[b"GET"]);
assert_eq!(f.server.stats.commands, 3);
}
#[test]
fn a_set_goes_from_bytes_to_bytes() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SADD", b"s", b"a", b"b", b"c"]), ":3\r\n");
assert_eq!(f.run(&[b"SADD", b"s", b"b", b"d"]), ":1\r\n");
assert_eq!(f.run(&[b"SCARD", b"s"]), ":4\r\n");
assert_eq!(f.run(&[b"SISMEMBER", b"s", b"a"]), ":1\r\n");
assert_eq!(f.run(&[b"SISMEMBER", b"s", b"z"]), ":0\r\n");
assert_eq!(f.run(&[b"TYPE", b"s"]), "+set\r\n");
assert_eq!(
f.run(&[b"SMISMEMBER", b"s", b"a", b"z", b"d"]),
"*3\r\n:1\r\n:0\r\n:1\r\n"
);
assert_eq!(f.run(&[b"SREM", b"s", b"a", b"z"]), ":1\r\n");
assert_eq!(f.run(&[b"SCARD", b"s"]), ":3\r\n");
}
#[test]
fn a_set_command_at_a_key_that_is_not_there_answers_empty() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SCARD", b"nope"]), ":0\r\n");
assert_eq!(f.run(&[b"SISMEMBER", b"nope", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"SREM", b"nope", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"SMEMBERS", b"nope"]), "*0\r\n");
assert_eq!(
f.run(&[b"SMISMEMBER", b"nope", b"a", b"b"]),
"*2\r\n:0\r\n:0\r\n"
);
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n", "and made nothing");
}
#[test]
fn smembers_answers_a_set_on_resp3_and_an_array_on_resp2() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"s", b"one"]);
assert_eq!(f.run(&[b"SMEMBERS", b"s"]), "*1\r\n$3\r\none\r\n");
f.run(&[b"HELLO", b"3"]);
assert_eq!(f.run(&[b"SMEMBERS", b"s"]), "~1\r\n$3\r\none\r\n");
}
#[test]
fn an_integer_member_comes_back_as_the_digits_it_never_stored() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"s", b"42"]);
assert_eq!(f.run(&[b"SMEMBERS", b"s"]), "*1\r\n$2\r\n42\r\n");
assert_eq!(f.run(&[b"SISMEMBER", b"s", b"42"]), ":1\r\n");
assert_eq!(
f.run(&[b"SISMEMBER", b"s", b"042"]),
":0\r\n",
"the member is the bytes and not the number they parse to"
);
}
#[test]
fn the_wrong_command_at_the_wrong_type_says_so_both_ways() {
let mut f = Fixture::new();
f.run(&[b"SET", b"str", b"v"]);
f.run(&[b"SADD", b"set", b"a"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
assert_eq!(f.run(&[b"SADD", b"str", b"a"]), wrong);
assert_eq!(f.run(&[b"SCARD", b"str"]), wrong);
assert_eq!(f.run(&[b"SMEMBERS", b"str"]), wrong);
assert_eq!(f.run(&[b"SMISMEMBER", b"str", b"a"]), wrong);
assert_eq!(f.run(&[b"GET", b"set"]), wrong);
assert_eq!(f.run(&[b"APPEND", b"set", b"x"]), wrong);
assert_eq!(f.run(&[b"INCR", b"set"]), wrong);
assert_eq!(f.run(&[b"STRLEN", b"set"]), wrong);
assert_eq!(
f.run(&[b"MGET", b"str", b"set", b"nope"]),
"*3\r\n$1\r\nv\r\n$-1\r\n$-1\r\n"
);
assert_eq!(f.run(&[b"SET", b"set", b"now a string"]), "+OK\r\n");
assert_eq!(f.run(&[b"TYPE", b"set"]), "+string\r\n");
}
#[test]
fn a_wrongtype_leaves_nothing_half_written() {
let mut f = Fixture::new();
f.run(&[b"SET", b"k", b"v"]);
let reply = f.run(&[b"SMISMEMBER", b"k", b"a", b"b"]);
assert!(reply.starts_with("-WRONGTYPE"), "got {reply}");
assert!(!reply.contains('*'), "an array header went out in front");
}
#[test]
fn emptying_a_set_takes_the_key_with_it() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"s", b"a", b"b"]);
assert_eq!(f.run(&[b"DBSIZE"]), ":1\r\n");
assert_eq!(f.run(&[b"SREM", b"s", b"a", b"b"]), ":2\r\n");
assert_eq!(f.run(&[b"EXISTS", b"s"]), ":0\r\n");
assert_eq!(f.run(&[b"TYPE", b"s"]), "+none\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
}
fn split_scan(reply: &str) -> (String, Vec<String>) {
let mut lines = reply.split("\r\n");
assert_eq!(lines.next(), Some("*2"), "got {reply}");
lines.next().expect("the cursor header");
let cursor = lines.next().expect("the cursor").to_owned();
let header = lines.next().expect("the member header");
let n: usize = header[1..].parse().expect("a member count");
let mut members = Vec::with_capacity(n);
for _ in 0..n {
lines.next().expect("a member header");
members.push(lines.next().expect("a member").to_owned());
}
(cursor, members)
}
#[test]
fn popping_takes_a_member_off_the_set_and_hands_it_back() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"s", b"a", b"b", b"c", b"d"]);
let one = f.run(&[b"SPOP", b"s"]);
assert!(
["$1\r\na\r\n", "$1\r\nb\r\n", "$1\r\nc\r\n", "$1\r\nd\r\n"].contains(&one.as_str()),
"got {one}"
);
assert_eq!(f.run(&[b"SCARD", b"s"]), ":3\r\n");
let (_, rest) = ("", f.run(&[b"SPOP", b"s", b"3"]));
assert!(rest.starts_with("*3\r\n"), "got {rest}");
assert_eq!(f.run(&[b"EXISTS", b"s"]), ":0\r\n");
assert_eq!(f.run(&[b"SPOP", b"s"]), "$-1\r\n");
assert_eq!(f.run(&[b"SPOP", b"s", b"2"]), "*0\r\n");
}
#[test]
fn the_two_draws_disagree_about_the_reply_type_and_they_are_right_to() {
let mut f = Fixture::new();
f.run(&[b"HELLO", b"3"]);
f.run(&[b"SADD", b"s", b"a", b"b", b"c"]);
assert!(f.run(&[b"SPOP", b"s", b"2"]).starts_with("~2\r\n"));
assert!(f.run(&[b"SRANDMEMBER", b"s", b"1"]).starts_with("*1\r\n"));
f.run(&[b"SADD", b"one", b"z"]);
assert_eq!(
f.run(&[b"SRANDMEMBER", b"one", b"-3"]),
"*3\r\n$1\r\nz\r\n$1\r\nz\r\n$1\r\nz\r\n"
);
}
#[test]
fn drawing_a_member_removes_nothing_and_says_nil_at_a_missing_key() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"s", b"only"]);
assert_eq!(f.run(&[b"SRANDMEMBER", b"s"]), "$4\r\nonly\r\n");
assert_eq!(f.run(&[b"SRANDMEMBER", b"s"]), "$4\r\nonly\r\n");
assert_eq!(f.run(&[b"SCARD", b"s"]), ":1\r\n");
assert_eq!(f.run(&[b"SRANDMEMBER", b"nope"]), "$-1\r\n");
assert_eq!(f.run(&[b"SRANDMEMBER", b"nope", b"3"]), "*0\r\n");
assert_eq!(f.run(&[b"SRANDMEMBER", b"nope", b"-3"]), "*0\r\n");
assert_eq!(f.run(&[b"SRANDMEMBER", b"s", b"9"]), "*1\r\n$4\r\nonly\r\n");
}
#[test]
fn a_pop_count_that_is_not_a_positive_number_says_so() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"s", b"a"]);
let bad = "-ERR value is out of range, must be positive\r\n";
assert_eq!(f.run(&[b"SPOP", b"s", b"-1"]), bad);
assert_eq!(f.run(&[b"SPOP", b"s", b"abc"]), bad);
assert_eq!(f.run(&[b"SCARD", b"s"]), ":1\r\n", "and took nothing");
assert_eq!(f.run(&[b"SPOP", b"s", b"0"]), "*0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"s"]), ":1\r\n");
}
#[test]
fn a_scan_walks_a_set_of_any_size_exactly_once() {
let mut f = Fixture::new();
let members: Vec<Vec<u8>> = (0..300).map(|i| format!("m{i}").into_bytes()).collect();
let args: Vec<&[u8]> = [&b"SADD"[..], &b"s"[..]]
.into_iter()
.chain(members.iter().map(Vec::as_slice))
.collect();
f.run(&args);
let mut seen = Vec::new();
let mut cursor = "0".to_owned();
loop {
let reply = f.run(&[b"SSCAN", b"s", cursor.as_bytes()]);
let (next, got) = split_scan(&reply);
seen.extend(got);
cursor = next;
if cursor == "0" {
break;
}
}
seen.sort();
seen.dedup();
assert_eq!(seen.len(), 300, "a walk saw a member twice or missed one");
f.run(&[b"SADD", b"small", b"a", b"b", b"c"]);
let (cursor, got) = split_scan(&f.run(&[b"SSCAN", b"small", b"0", b"COUNT", b"1"]));
assert_eq!(cursor, "0");
assert_eq!(got.len(), 3);
assert_eq!(f.run(&[b"SSCAN", b"nope", b"0"]), "*2\r\n$1\r\n0\r\n*0\r\n");
}
#[test]
fn a_scan_takes_match_and_count_and_refuses_anything_else() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"s", b"aa", b"ab", b"ba", b"12", b"13"]);
let (_, got) = split_scan(&f.run(&[b"SSCAN", b"s", b"0", b"MATCH", b"a*"]));
let mut got = got;
got.sort();
assert_eq!(got, ["aa", "ab"]);
let (_, got) = split_scan(&f.run(&[b"SSCAN", b"s", b"0", b"MATCH", b"1?"]));
let mut got = got;
got.sort();
assert_eq!(got, ["12", "13"]);
assert_eq!(f.run(&[b"SSCAN", b"s", b"abc"]), "-ERR invalid cursor\r\n");
assert_eq!(f.run(&[b"SSCAN", b"s", b"-1"]), "-ERR invalid cursor\r\n");
assert_eq!(
f.run(&[b"SSCAN", b"s", b"0", b"NOPE", b"1"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"SSCAN", b"s", b"0", b"COUNT", b"0"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn moving_a_member_takes_it_off_one_set_and_puts_it_on_another() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"src", b"a", b"b"]);
f.run(&[b"SADD", b"dst", b"c"]);
assert_eq!(f.run(&[b"SMOVE", b"src", b"dst", b"a"]), ":1\r\n");
assert_eq!(f.run(&[b"SISMEMBER", b"src", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"SISMEMBER", b"dst", b"a"]), ":1\r\n");
assert_eq!(f.run(&[b"SMOVE", b"src", b"dst", b"zz"]), ":0\r\n");
assert_eq!(f.run(&[b"SCARD", b"dst"]), ":2\r\n");
assert_eq!(f.run(&[b"SMOVE", b"src", b"fresh", b"b"]), ":1\r\n");
assert_eq!(f.run(&[b"EXISTS", b"src"]), ":0\r\n");
assert_eq!(f.run(&[b"SMEMBERS", b"fresh"]), "*1\r\n$1\r\nb\r\n");
}
#[test]
fn moving_checks_the_types_in_the_order_redis_checks_them() {
let mut f = Fixture::new();
f.run(&[b"SET", b"str", b"v"]);
f.run(&[b"SADD", b"set", b"a"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
assert_eq!(f.run(&[b"SMOVE", b"nope", b"str", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"SMOVE", b"str", b"set", b"a"]), wrong);
assert_eq!(f.run(&[b"SMOVE", b"set", b"str", b"a"]), wrong);
assert_eq!(f.run(&[b"SPOP", b"str"]), wrong);
assert_eq!(f.run(&[b"SRANDMEMBER", b"str"]), wrong);
assert_eq!(f.run(&[b"SSCAN", b"str", b"0"]), wrong);
assert_eq!(
f.run(&[b"SISMEMBER", b"set", b"a"]),
":1\r\n",
"and none of that moved anything"
);
}
#[test]
fn a_scan_leaves_nothing_half_written_when_its_arguments_are_wrong() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"s", b"a"]);
for bad in [
&[b"SSCAN".as_slice(), b"s", b"abc"][..],
&[b"SSCAN".as_slice(), b"s", b"0", b"COUNT", b"nope"][..],
&[b"SSCAN".as_slice(), b"s", b"0", b"MATCH"][..],
] {
let reply = f.run(bad);
assert!(reply.starts_with("-ERR"), "got {reply}");
assert!(!reply.contains('*'), "an array header went out in front");
}
}
#[test]
fn a_hash_writes_reads_and_deletes_its_fields() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"HSET", b"h", b"a", b"1", b"b", b"2"]), ":2\r\n");
assert_eq!(f.run(&[b"HSET", b"h", b"a", b"9"]), ":0\r\n", "a was there");
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$1\r\n9\r\n");
assert_eq!(f.run(&[b"HGET", b"h", b"nope"]), "$-1\r\n");
assert_eq!(f.run(&[b"HGET", b"nokey", b"a"]), "$-1\r\n");
assert_eq!(f.run(&[b"HLEN", b"h"]), ":2\r\n");
assert_eq!(f.run(&[b"HEXISTS", b"h", b"a"]), ":1\r\n");
assert_eq!(f.run(&[b"HEXISTS", b"h", b"nope"]), ":0\r\n");
assert_eq!(f.run(&[b"HSTRLEN", b"h", b"a"]), ":1\r\n");
assert_eq!(f.run(&[b"HSTRLEN", b"h", b"nope"]), ":0\r\n");
assert_eq!(f.run(&[b"HGET", b"h", b"2"]), "$-1\r\n");
assert_eq!(f.run(&[b"HDEL", b"h", b"a", b"nope"]), ":1\r\n");
assert_eq!(f.run(&[b"HDEL", b"h", b"b"]), ":1\r\n");
assert_eq!(
f.run(&[b"EXISTS", b"h"]),
":0\r\n",
"and losing the last field lost the key"
);
}
#[test]
fn hgetall_answers_a_map_on_resp3_and_the_same_pairs_flat_on_resp2() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1"]);
assert_eq!(f.run(&[b"HGETALL", b"h"]), "*2\r\n$1\r\na\r\n$1\r\n1\r\n");
assert_eq!(f.run(&[b"HGETALL", b"nokey"]), "*0\r\n");
assert_eq!(f.run(&[b"HKEYS", b"h"]), "*1\r\n$1\r\na\r\n");
assert_eq!(f.run(&[b"HVALS", b"h"]), "*1\r\n$1\r\n1\r\n");
assert_eq!(f.run(&[b"HKEYS", b"nokey"]), "*0\r\n");
f.run(&[b"HELLO", b"3"]);
assert_eq!(f.run(&[b"HGETALL", b"h"]), "%1\r\n$1\r\na\r\n$1\r\n1\r\n");
assert_eq!(
f.run(&[b"HGETALL", b"nokey"]),
"%0\r\n",
"a missing key is the empty hash and never a nil"
);
assert_eq!(
f.run(&[b"HKEYS", b"h"]),
"*1\r\n$1\r\na\r\n",
"and the two that answer one side stay arrays"
);
}
#[test]
fn hmget_answers_once_per_field_and_hmset_answers_ok() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"HMSET", b"h", b"a", b"1", b"c", b"3"]), "+OK\r\n");
assert_eq!(
f.run(&[b"HMGET", b"h", b"a", b"b", b"c"]),
"*3\r\n$1\r\n1\r\n$-1\r\n$1\r\n3\r\n",
"the reply is positional, so b is a nil and not a gap"
);
assert_eq!(
f.run(&[b"HMGET", b"nokey", b"a", b"b"]),
"*2\r\n$-1\r\n$-1\r\n",
"and a missing key is all nils rather than an empty array"
);
assert_eq!(f.run(&[b"HSETNX", b"h", b"a", b"9"]), ":0\r\n");
assert_eq!(f.run(&[b"HSETNX", b"h", b"z", b"9"]), ":1\r\n");
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$1\r\n1\r\n");
}
#[test]
fn a_hash_counts_up_and_says_so_when_it_cannot() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"HINCRBY", b"h", b"n", b"5"]), ":5\r\n");
assert_eq!(f.run(&[b"HINCRBY", b"h", b"n", b"-7"]), ":-2\r\n");
assert_eq!(f.run(&[b"HGET", b"h", b"n"]), "$2\r\n-2\r\n");
assert_eq!(
f.run(&[b"HINCRBYFLOAT", b"h", b"f", b"10.5"]),
"$4\r\n10.5\r\n",
"a bulk string and not a double, on both protocols"
);
f.run(&[b"HSET", b"h", b"s", b"words"]);
let bad = f.run(&[b"HINCRBY", b"h", b"s", b"1"]);
assert!(
bad.starts_with("-ERR hash value is not an integer"),
"{bad}"
);
let bad = f.run(&[b"HINCRBY", b"h", b"n", b"nope"]);
assert!(
bad.starts_with("-ERR value is not an integer"),
"a bad argument is not yet a hash value, {bad}"
);
assert_eq!(
f.run(&[b"HGET", b"h", b"s"]),
"$5\r\nwords\r\n",
"and neither of them wrote anything"
);
}
#[test]
fn a_hash_scan_walks_every_pair_once_and_novalues_drops_half_of_it() {
let mut f = Fixture::new();
for i in 0..500 {
let field = format!("field-{i}");
let value = format!("value-{i}");
f.run(&[b"HSET", b"h", field.as_bytes(), value.as_bytes()]);
}
let mut seen: Vec<String> = Vec::new();
let mut cursor = "0".to_owned();
loop {
let reply = f.run(&[b"HSCAN", b"h", cursor.as_bytes(), b"COUNT", b"32"]);
let (next, items) = scan_reply(&reply);
assert_eq!(items.len() % 2, 0, "a pair went out half written");
for pair in items.chunks(2) {
assert_eq!(
pair[0].strip_prefix("field-"),
pair[1].strip_prefix("value-"),
"a field came back with someone else's value"
);
seen.push(pair[0].clone());
}
cursor = next;
if cursor == "0" {
break;
}
}
seen.sort();
seen.dedup();
assert_eq!(seen.len(), 500, "every field once and only once");
let (_, items) = scan_reply(&f.run(&[b"HSCAN", b"h", b"0", b"NOVALUES", b"COUNT", b"32"]));
assert!(
items.iter().all(|s| s.starts_with("field-")),
"NOVALUES still sent the values"
);
let (_, one) = scan_reply(&f.run(&[
b"HSCAN",
b"h",
b"0",
b"MATCH",
b"field-499",
b"COUNT",
b"1000",
]));
assert_eq!(one, ["field-499", "value-499"], "MATCH is on the field");
}
#[test]
fn hrandfield_draws_what_it_was_asked_for_and_nests_values_on_resp3() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1"]);
assert_eq!(f.run(&[b"HRANDFIELD", b"h"]), "$1\r\na\r\n");
assert_eq!(f.run(&[b"HRANDFIELD", b"nokey"]), "$-1\r\n");
assert_eq!(f.run(&[b"HRANDFIELD", b"nokey", b"3"]), "*0\r\n");
assert_eq!(
f.run(&[b"HRANDFIELD", b"h", b"3"]),
"*1\r\n$1\r\na\r\n",
"a positive count is capped at the size of the hash"
);
assert_eq!(
f.run(&[b"HRANDFIELD", b"h", b"-3"]),
"*3\r\n$1\r\na\r\n$1\r\na\r\n$1\r\na\r\n",
"and a negative one repeats itself"
);
assert_eq!(
f.run(&[b"HRANDFIELD", b"h", b"1", b"WITHVALUES"]),
"*2\r\n$1\r\na\r\n$1\r\n1\r\n",
"flat on RESP2"
);
f.run(&[b"HELLO", b"3"]);
assert_eq!(
f.run(&[b"HRANDFIELD", b"h", b"1", b"WITHVALUES"]),
"*1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n",
"and nested on RESP3, but still an array and never a map"
);
}
#[test]
fn every_hash_command_says_wrongtype_and_writes_nothing() {
let mut f = Fixture::new();
f.run(&[b"SET", b"str", b"v"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
for cmd in [
&[b"HSET".as_slice(), b"str", b"f", b"v"][..],
&[b"HMSET".as_slice(), b"str", b"f", b"v"][..],
&[b"HSETNX".as_slice(), b"str", b"f", b"v"][..],
&[b"HGET".as_slice(), b"str", b"f"][..],
&[b"HMGET".as_slice(), b"str", b"f"][..],
&[b"HDEL".as_slice(), b"str", b"f"][..],
&[b"HLEN".as_slice(), b"str"][..],
&[b"HEXISTS".as_slice(), b"str", b"f"][..],
&[b"HSTRLEN".as_slice(), b"str", b"f"][..],
&[b"HGETALL".as_slice(), b"str"][..],
&[b"HKEYS".as_slice(), b"str"][..],
&[b"HVALS".as_slice(), b"str"][..],
&[b"HINCRBY".as_slice(), b"str", b"f", b"1"][..],
&[b"HINCRBYFLOAT".as_slice(), b"str", b"f", b"1"][..],
&[b"HRANDFIELD".as_slice(), b"str"][..],
&[b"HRANDFIELD".as_slice(), b"str", b"2"][..],
&[b"HSCAN".as_slice(), b"str", b"0"][..],
] {
let reply = f.run(cmd);
assert_eq!(reply, wrong, "{:?}", cmd[0]);
}
assert_eq!(
f.run(&[b"GET", b"str"]),
"$1\r\nv\r\n",
"and none of them touched the value"
);
}
#[test]
fn a_hash_scan_leaves_nothing_half_written_when_its_arguments_are_wrong() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"f", b"v"]);
for bad in [
&[b"HSCAN".as_slice(), b"h", b"abc"][..],
&[b"HSCAN".as_slice(), b"h", b"0", b"COUNT", b"nope"][..],
&[b"HSCAN".as_slice(), b"h", b"0", b"COUNT", b"0"][..],
&[b"HSCAN".as_slice(), b"h", b"0", b"MATCH"][..],
] {
let reply = f.run(bad);
assert!(reply.starts_with("-ERR"), "got {reply}");
assert!(!reply.contains('*'), "an array header went out in front");
}
}
#[test]
fn a_field_deadline_goes_on_and_comes_back_in_all_four_units() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1", b"b", b"2"]);
assert_eq!(
f.run(&[b"HEXPIRE", b"h", b"100", b"FIELDS", b"1", b"a"]),
"*1\r\n:1\r\n"
);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"3", b"a", b"b", b"nope"]),
"*3\r\n:100\r\n:-1\r\n:-2\r\n",
"one answer per field, and the two sentinels are TTL's own"
);
let ms = int_reply(&f.run(&[b"HPTTL", b"h", b"FIELDS", b"1", b"a"]));
assert!((99_000..=100_000).contains(&ms), "got {ms}");
let at = int_reply(&f.run(&[b"HEXPIRETIME", b"h", b"FIELDS", b"1", b"a"]));
let at_ms = int_reply(&f.run(&[b"HPEXPIRETIME", b"h", b"FIELDS", b"1", b"a"]));
assert_eq!(at, at_ms.div_euclid(1000) + i64::from(at_ms % 1000 != 0));
assert!(at_ms > 1_700_000_000_000, "an absolute moment, got {at_ms}");
assert_eq!(
f.run(&[b"HPERSIST", b"h", b"FIELDS", b"3", b"a", b"b", b"nope"]),
"*3\r\n:1\r\n:-1\r\n:-2\r\n",
"one for the deadline taken off, and it does not say what it was"
);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:-1\r\n"
);
assert_eq!(
f.run(&[b"HGET", b"h", b"a"]),
"$1\r\n1\r\n",
"and the field is still there with the value it had"
);
}
#[test]
fn a_deadline_that_has_already_gone_deletes_the_field_now() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1", b"b", b"2"]);
assert_eq!(
f.run(&[b"HEXPIREAT", b"h", b"1", b"FIELDS", b"1", b"a"]),
"*1\r\n:2\r\n",
"two, and not one, because nothing was stored"
);
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$-1\r\n");
assert_eq!(f.run(&[b"HLEN", b"h"]), ":1\r\n");
assert_eq!(
f.run(&[b"HPEXPIREAT", b"h", b"1", b"FIELDS", b"1", b"b"]),
"*1\r\n:2\r\n"
);
assert_eq!(
f.run(&[b"EXISTS", b"h"]),
":0\r\n",
"and the last field going took the key with it"
);
f.run(&[b"HSET", b"h", b"a", b"1"]);
assert_eq!(
f.run(&[b"HEXPIRE", b"h", b"0", b"FIELDS", b"1", b"a"]),
"*1\r\n:2\r\n"
);
}
#[test]
fn a_field_is_gone_once_its_moment_passes() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1", b"b", b"2"]);
assert_eq!(
f.run(&[b"HPEXPIRE", b"h", b"20", b"FIELDS", b"1", b"a"]),
"*1\r\n:1\r\n"
);
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$1\r\n1\r\n", "not yet");
f.server.db(0).clock_mut().advance(60);
assert_eq!(f.run(&[b"HLEN", b"h"]), ":1\r\n");
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$-1\r\n");
assert_eq!(
f.run(&[b"HGETALL", b"h"]),
"*2\r\n$1\r\nb\r\n$1\r\n2\r\n",
"and the walks do not hand back a field that has expired"
);
}
#[test]
fn a_missing_key_answers_the_no_field_sentinel_for_every_field() {
let mut f = Fixture::new();
for cmd in [
&[
b"HEXPIRE".as_slice(),
b"nokey",
b"100",
b"FIELDS",
b"2",
b"a",
b"b",
][..],
&[b"HTTL".as_slice(), b"nokey", b"FIELDS", b"2", b"a", b"b"][..],
&[b"HPTTL".as_slice(), b"nokey", b"FIELDS", b"2", b"a", b"b"][..],
&[
b"HEXPIRETIME".as_slice(),
b"nokey",
b"FIELDS",
b"2",
b"a",
b"b",
][..],
&[
b"HPERSIST".as_slice(),
b"nokey",
b"FIELDS",
b"2",
b"a",
b"b",
][..],
] {
assert_eq!(f.run(cmd), "*2\r\n:-2\r\n:-2\r\n", "{:?}", cmd[0]);
}
}
#[test]
fn writing_a_field_clears_the_deadline_that_was_on_it() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1"]);
f.run(&[b"HEXPIRE", b"h", b"100", b"FIELDS", b"1", b"a"]);
f.run(&[b"HSET", b"h", b"a", b"2"]);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:-1\r\n",
"Redis has done this since 7.4, and it is why HGETEX exists"
);
}
#[test]
fn the_four_conditions_reach_the_store_the_way_they_were_written() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1"]);
assert_eq!(
f.run(&[b"HEXPIRE", b"h", b"100", b"XX", b"FIELDS", b"1", b"a"]),
"*1\r\n:0\r\n",
"XX on a field with no deadline changes nothing"
);
assert_eq!(
f.run(&[b"HEXPIRE", b"h", b"100", b"NX", b"FIELDS", b"1", b"a"]),
"*1\r\n:1\r\n"
);
assert_eq!(
f.run(&[b"HEXPIRE", b"h", b"200", b"NX", b"FIELDS", b"1", b"a"]),
"*1\r\n:0\r\n",
"and NX will not move one that is already there"
);
assert_eq!(
f.run(&[b"HEXPIRE", b"h", b"50", b"GT", b"FIELDS", b"1", b"a"]),
"*1\r\n:0\r\n"
);
assert_eq!(
f.run(&[b"HEXPIRE", b"h", b"500", b"GT", b"FIELDS", b"1", b"a"]),
"*1\r\n:1\r\n"
);
assert_eq!(
f.run(&[b"HEXPIRE", b"h", b"50", b"LT", b"FIELDS", b"1", b"a"]),
"*1\r\n:1\r\n"
);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:50\r\n"
);
}
#[test]
fn the_field_ttl_family_leaves_nothing_half_written_on_a_bad_argument() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1"]);
for (bad, want) in [
(
&[b"HEXPIRE".as_slice(), b"h", b"-1", b"FIELDS", b"1", b"a"][..],
"-ERR invalid expire time, must be >= 0",
),
(
&[
b"HEXPIRE".as_slice(),
b"h",
b"9999999999999999",
b"FIELDS",
b"1",
b"a",
][..],
"-ERR invalid expire time in 'hexpire' command",
),
(
&[b"HEXPIRE".as_slice(), b"h", b"100", b"FIELD", b"1", b"a"][..],
"-ERR wrong number of arguments for 'hexpire' command",
),
(
&[b"HEXPIRE".as_slice(), b"h", b"100", b"FIELDS", b"0", b"a"][..],
"-ERR Parameter `numFields` should be greater than 0",
),
(
&[b"HEXPIRE".as_slice(), b"h", b"100", b"FIELDS", b"2", b"a"][..],
"-ERR wrong number of arguments",
),
(
&[b"HTTL".as_slice(), b"h", b"FIELDS", b"3", b"a", b"b"][..],
"-ERR wrong number of arguments",
),
] {
let reply = f.run(bad);
assert!(reply.starts_with(want), "wanted {want}, got {reply}");
assert!(!reply.contains('*'), "an array header went out in front");
}
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:-1\r\n",
"and not one of them put a deadline on anything"
);
}
#[test]
fn every_field_ttl_command_says_wrongtype_and_writes_nothing() {
let mut f = Fixture::new();
f.run(&[b"SET", b"str", b"v"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
for cmd in [
&[b"HEXPIRE".as_slice(), b"str", b"100", b"FIELDS", b"1", b"f"][..],
&[
b"HPEXPIRE".as_slice(),
b"str",
b"100",
b"FIELDS",
b"1",
b"f",
][..],
&[
b"HEXPIREAT".as_slice(),
b"str",
b"9999999999",
b"FIELDS",
b"1",
b"f",
][..],
&[
b"HPEXPIREAT".as_slice(),
b"str",
b"9999999999999",
b"FIELDS",
b"1",
b"f",
][..],
&[b"HTTL".as_slice(), b"str", b"FIELDS", b"1", b"f"][..],
&[b"HPTTL".as_slice(), b"str", b"FIELDS", b"1", b"f"][..],
&[b"HEXPIRETIME".as_slice(), b"str", b"FIELDS", b"1", b"f"][..],
&[b"HPEXPIRETIME".as_slice(), b"str", b"FIELDS", b"1", b"f"][..],
&[b"HPERSIST".as_slice(), b"str", b"FIELDS", b"1", b"f"][..],
] {
assert_eq!(f.run(cmd), wrong, "{:?}", cmd[0]);
}
assert_eq!(
f.run(&[b"GET", b"str"]),
"$1\r\nv\r\n",
"and none of them touched the value"
);
}
#[test]
fn hgetdel_hands_the_value_out_and_then_takes_the_field() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1", b"b", b"2"]);
assert_eq!(
f.run(&[b"HGETDEL", b"h", b"FIELDS", b"2", b"a", b"nope"]),
"*2\r\n$1\r\n1\r\n$-1\r\n",
"positional, so the field that was not there is a nil in its place"
);
assert_eq!(f.run(&[b"HLEN", b"h"]), ":1\r\n");
assert_eq!(
f.run(&[b"HGETDEL", b"nokey", b"FIELDS", b"1", b"a"]),
"*1\r\n$-1\r\n"
);
assert_eq!(
f.run(&[b"HGETDEL", b"h", b"FIELDS", b"1", b"b"]),
"*1\r\n$1\r\n2\r\n"
);
assert_eq!(
f.run(&[b"EXISTS", b"h"]),
":0\r\n",
"and the last field took the key"
);
}
#[test]
fn hgetex_reads_and_moves_the_deadline_in_one_command() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1"]);
assert_eq!(
f.run(&[b"HGETEX", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n$1\r\n1\r\n"
);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:-1\r\n",
"no option means leave it alone, which is the one place this is not GETEX"
);
f.run(&[b"HGETEX", b"h", b"EX", b"100", b"FIELDS", b"1", b"a"]);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:100\r\n"
);
f.run(&[b"HGETEX", b"h", b"FIELDS", b"1", b"a"]);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:100\r\n",
"and a plain read really does leave it alone"
);
assert_eq!(
f.run(&[b"HGETEX", b"h", b"PERSIST", b"FIELDS", b"1", b"a"]),
"*1\r\n$1\r\n1\r\n"
);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:-1\r\n"
);
assert_eq!(
f.run(&[b"HGETEX", b"h", b"EXAT", b"1", b"FIELDS", b"1", b"a"]),
"*1\r\n$1\r\n1\r\n",
"the value goes out before the deadline that has already gone is applied"
);
assert_eq!(f.run(&[b"EXISTS", b"h"]), ":0\r\n");
assert_eq!(
f.run(&[b"HGETEX", b"nokey", b"EX", b"100", b"FIELDS", b"1", b"a"]),
"*1\r\n$-1\r\n"
);
}
#[test]
fn hsetex_writes_all_of_it_or_none_of_it() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"HSETEX", b"h", b"FIELDS", b"1", b"a", b"1"]),
":1\r\n"
);
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$1\r\n1\r\n");
assert_eq!(
f.run(&[
b"HSETEX", b"h", b"FNX", b"FIELDS", b"2", b"a", b"9", b"new", b"9"
]),
":0\r\n",
"FNX wants every field named to be missing"
);
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$1\r\n1\r\n");
assert_eq!(
f.run(&[b"HEXISTS", b"h", b"new"]),
":0\r\n",
"and none of the list was written"
);
assert_eq!(
f.run(&[
b"HSETEX", b"h", b"FXX", b"FIELDS", b"2", b"a", b"9", b"nope", b"9"
]),
":0\r\n",
"and FXX wants every one of them to be there"
);
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$1\r\n1\r\n");
assert_eq!(
f.run(&[b"HSETEX", b"h", b"FXX", b"FIELDS", b"1", b"a", b"9"]),
":1\r\n"
);
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$1\r\n9\r\n");
assert_eq!(
f.run(&[b"HSETEX", b"gone", b"FXX", b"FIELDS", b"1", b"a", b"1"]),
":0\r\n"
);
assert_eq!(
f.run(&[b"EXISTS", b"gone"]),
":0\r\n",
"a key with no fields cannot meet FXX and is not created trying"
);
}
#[test]
fn hsetex_clears_the_deadline_unless_it_is_told_to_keep_it() {
let mut f = Fixture::new();
f.run(&[b"HSETEX", b"h", b"EX", b"100", b"FIELDS", b"1", b"a", b"1"]);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:100\r\n"
);
f.run(&[b"HSETEX", b"h", b"KEEPTTL", b"FIELDS", b"1", b"a", b"2"]);
assert_eq!(f.run(&[b"HGET", b"h", b"a"]), "$1\r\n2\r\n");
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:100\r\n",
"KEEPTTL put back what the write cleared"
);
f.run(&[b"HSETEX", b"h", b"FIELDS", b"1", b"a", b"3"]);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:-1\r\n",
"and without it a write clears the deadline the way HSET does"
);
assert_eq!(
f.run(&[
b"HSETEX", b"h", b"PX", b"100000", b"FXX", b"FIELDS", b"1", b"a", b"4"
]),
":1\r\n"
);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:100\r\n"
);
assert_eq!(
f.run(&[b"HSETEX", b"h", b"EXAT", b"1", b"FIELDS", b"1", b"a", b"5"]),
":1\r\n",
"written, and not the separate code the HEXPIRE family has for this"
);
assert_eq!(
f.run(&[b"EXISTS", b"h"]),
":0\r\n",
"and storing it and then removing it emptied the hash"
);
}
#[test]
fn the_last_three_hash_commands_word_their_mistakes_their_own_way() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"a", b"1"]);
for (bad, want) in [
(
&[b"HGETDEL".as_slice(), b"h", b"FIELDS", b"0", b"a"][..],
"-ERR Number of fields must be a positive integer",
),
(
&[b"HGETDEL".as_slice(), b"h", b"FIELDS", b"2", b"a"][..],
"-ERR The `numfields` parameter must match the number of arguments",
),
(
&[b"HGETDEL".as_slice(), b"h", b"FIELD", b"1", b"a"][..],
"-ERR Mandatory argument FIELDS is missing or not at the right position",
),
(
&[b"HGETEX".as_slice(), b"h", b"FIELDS", b"0", b"a"][..],
"-ERR invalid number of fields",
),
(
&[b"HGETEX".as_slice(), b"h", b"FIELDS", b"2", b"a"][..],
"-ERR wrong number of arguments",
),
(
&[b"HGETEX".as_slice(), b"h", b"FIELD", b"1", b"a"][..],
"-ERR unknown argument: FIELD",
),
(
&[
b"HGETEX".as_slice(),
b"h",
b"KEEPTTL",
b"FIELDS",
b"1",
b"a",
][..],
"-ERR unknown argument: KEEPTTL",
),
(
&[
b"HGETEX".as_slice(),
b"h",
b"EX",
b"100",
b"PERSIST",
b"FIELDS",
b"1",
b"a",
][..],
"-ERR Only one of EX, PX, EXAT, PXAT or PERSIST arguments can be specified",
),
(
&[
b"HSETEX".as_slice(),
b"h",
b"EX",
b"1",
b"KEEPTTL",
b"FIELDS",
b"1",
b"a",
b"1",
][..],
"-ERR Only one of EX, PX, EXAT, PXAT or KEEPTTL arguments can be specified",
),
(
&[
b"HSETEX".as_slice(),
b"h",
b"FNX",
b"FXX",
b"FIELDS",
b"1",
b"a",
b"1",
][..],
"-ERR Only one of FXX or FNX arguments can be specified",
),
(
&[
b"HSETEX".as_slice(),
b"h",
b"FIELDS",
b"2",
b"a",
b"1",
b"b",
][..],
"-ERR wrong number of arguments",
),
(
&[
b"HGETEX".as_slice(),
b"h",
b"EX",
b"-1",
b"FIELDS",
b"1",
b"a",
][..],
"-ERR invalid expire time, must be >= 0",
),
(
&[
b"HGETEX".as_slice(),
b"h",
b"PXAT",
b"99999999999999",
b"FIELDS",
b"1",
b"a",
][..],
"-ERR invalid expire time in 'hgetex' command",
),
(
&[
b"HSETEX".as_slice(),
b"h",
b"EX",
b"abc",
b"FIELDS",
b"1",
b"a",
b"1",
][..],
"-ERR value is not an integer or out of range",
),
] {
let reply = f.run(bad);
assert!(reply.starts_with(want), "wanted {want}, got {reply}");
assert!(!reply.contains('*'), "an array header went out in front");
}
assert_eq!(
f.run(&[b"HGET", b"h", b"a"]),
"$1\r\n1\r\n",
"and not one of them wrote anything"
);
assert_eq!(
f.run(&[b"HTTL", b"h", b"FIELDS", b"1", b"a"]),
"*1\r\n:-1\r\n"
);
}
#[test]
fn the_last_three_hash_commands_say_wrongtype_and_write_nothing() {
let mut f = Fixture::new();
f.run(&[b"SET", b"str", b"v"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
for cmd in [
&[b"HGETDEL".as_slice(), b"str", b"FIELDS", b"1", b"f"][..],
&[b"HGETEX".as_slice(), b"str", b"FIELDS", b"1", b"f"][..],
&[
b"HGETEX".as_slice(),
b"str",
b"EX",
b"100",
b"FIELDS",
b"1",
b"f",
][..],
&[b"HSETEX".as_slice(), b"str", b"FIELDS", b"1", b"f", b"v"][..],
] {
assert_eq!(f.run(cmd), wrong, "{:?}", cmd[0]);
}
assert_eq!(f.run(&[b"GET", b"str"]), "$1\r\nv\r\n");
}
fn int(reply: &str) -> i64 {
let body = reply
.strip_prefix(':')
.and_then(|s| s.strip_suffix("\r\n"))
.unwrap_or_else(|| panic!("wanted an integer, got {reply}"));
body.parse().expect("an integer")
}
fn int_reply(reply: &str) -> i64 {
let body = reply
.strip_prefix("*1\r\n:")
.and_then(|s| s.strip_suffix("\r\n"))
.unwrap_or_else(|| panic!("wanted one integer, got {reply}"));
body.parse().expect("an integer")
}
fn scan_reply(reply: &str) -> (String, Vec<String>) {
let mut lines = reply.split("\r\n");
assert_eq!(lines.next(), Some("*2"), "got {reply}");
lines.next().expect("the cursor header");
let cursor = lines.next().expect("a cursor").to_owned();
let header = lines.next().expect("an item count");
let n: usize = header[1..].parse().expect("a count");
let mut items = Vec::with_capacity(n);
for _ in 0..n {
lines.next().expect("an item header");
items.push(lines.next().expect("an item").to_owned());
}
(cursor, items)
}
fn sorted(reply: &str) -> Vec<String> {
let mut lines = reply.split("\r\n");
let header = lines.next().expect("a header");
assert!(
header.starts_with('*') || header.starts_with('~'),
"got {reply}"
);
let n: usize = header[1..].parse().expect("a member count");
let mut got = Vec::with_capacity(n);
for _ in 0..n {
lines.next().expect("a member header");
got.push(lines.next().expect("a member").to_owned());
}
got.sort();
got
}
#[test]
fn the_algebra_answers_what_the_sets_share_and_do_not() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"a", b"1", b"2", b"3"]);
f.run(&[b"SADD", b"b", b"2", b"3", b"4"]);
f.run(&[b"SADD", b"c", b"3", b"4", b"5"]);
assert_eq!(sorted(&f.run(&[b"SINTER", b"a", b"b", b"c"])), ["3"]);
assert_eq!(
sorted(&f.run(&[b"SUNION", b"a", b"b", b"c"])),
["1", "2", "3", "4", "5"]
);
assert_eq!(sorted(&f.run(&[b"SDIFF", b"a", b"b"])), ["1"]);
assert_eq!(sorted(&f.run(&[b"SINTER", b"a"])), ["1", "2", "3"]);
assert_eq!(f.run(&[b"SINTER", b"a", b"nope"]), "*0\r\n");
assert_eq!(sorted(&f.run(&[b"SUNION", b"a", b"nope"])), ["1", "2", "3"]);
assert_eq!(f.run(&[b"SDIFF", b"nope", b"a"]), "*0\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":3\r\n", "and none of it made a key");
}
#[test]
fn the_algebra_answers_a_set_on_resp3_and_an_array_on_resp2() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"a", b"x"]);
assert_eq!(f.run(&[b"SINTER", b"a"]), "*1\r\n$1\r\nx\r\n");
assert_eq!(f.run(&[b"SUNION", b"a"]), "*1\r\n$1\r\nx\r\n");
assert_eq!(f.run(&[b"SDIFF", b"a"]), "*1\r\n$1\r\nx\r\n");
f.run(&[b"HELLO", b"3"]);
assert_eq!(f.run(&[b"SINTER", b"a"]), "~1\r\n$1\r\nx\r\n");
assert_eq!(f.run(&[b"SUNION", b"a"]), "~1\r\n$1\r\nx\r\n");
assert_eq!(f.run(&[b"SDIFF", b"a"]), "~1\r\n$1\r\nx\r\n");
assert_eq!(f.run(&[b"SINTER", b"nope"]), "~0\r\n");
}
#[test]
fn a_store_form_writes_a_key_and_answers_how_big_it_is() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"a", b"1", b"2", b"3"]);
f.run(&[b"SADD", b"b", b"2", b"3", b"4"]);
assert_eq!(f.run(&[b"SINTERSTORE", b"d", b"a", b"b"]), ":2\r\n");
assert_eq!(sorted(&f.run(&[b"SMEMBERS", b"d"])), ["2", "3"]);
assert_eq!(f.run(&[b"SUNIONSTORE", b"d", b"a", b"b"]), ":4\r\n");
assert_eq!(sorted(&f.run(&[b"SMEMBERS", b"d"])), ["1", "2", "3", "4"]);
assert_eq!(f.run(&[b"SDIFFSTORE", b"d", b"a", b"b"]), ":1\r\n");
assert_eq!(f.run(&[b"SMEMBERS", b"d"]), "*1\r\n$1\r\n1\r\n");
assert_eq!(f.run(&[b"SDIFFSTORE", b"d", b"a", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"d"]), ":0\r\n");
assert_eq!(f.run(&[b"SINTERSTORE", b"a", b"a", b"b"]), ":2\r\n");
assert_eq!(sorted(&f.run(&[b"SMEMBERS", b"a"])), ["2", "3"]);
f.run(&[b"SET", b"str", b"v"]);
assert_eq!(f.run(&[b"SUNIONSTORE", b"str", b"b"]), ":3\r\n");
assert_eq!(f.run(&[b"TYPE", b"str"]), "+set\r\n");
}
#[test]
fn sintercard_counts_without_building_and_stops_at_a_limit() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"a", b"1", b"2", b"3", b"4"]);
f.run(&[b"SADD", b"b", b"2", b"3", b"4", b"5"]);
assert_eq!(f.run(&[b"SINTERCARD", b"2", b"a", b"b"]), ":3\r\n");
assert_eq!(
f.run(&[b"SINTERCARD", b"2", b"a", b"b", b"LIMIT", b"2"]),
":2\r\n"
);
assert_eq!(
f.run(&[b"SINTERCARD", b"2", b"a", b"b", b"LIMIT", b"0"]),
":3\r\n",
"a limit of zero is no limit"
);
assert_eq!(f.run(&[b"SINTERCARD", b"1", b"a"]), ":4\r\n");
assert_eq!(f.run(&[b"SINTERCARD", b"2", b"a", b"nope"]), ":0\r\n");
assert_eq!(
f.run(&[b"SINTERCARD", b"0", b"a"]),
"-ERR numkeys should be greater than 0\r\n"
);
assert_eq!(
f.run(&[b"SINTERCARD", b"abc", b"a"]),
"-ERR numkeys should be greater than 0\r\n"
);
assert_eq!(
f.run(&[b"SINTERCARD", b"3", b"a", b"b"]),
"-ERR Number of keys can't be greater than number of args\r\n"
);
assert_eq!(
f.run(&[b"SINTERCARD", b"2", b"a", b"b", b"LIMIT", b"-1"]),
"-ERR LIMIT can't be negative\r\n"
);
assert_eq!(
f.run(&[b"SINTERCARD", b"2", b"a", b"b", b"NOPE", b"1"]),
"-ERR syntax error\r\n"
);
f.run(&[b"SADD", b"LIMIT", b"2"]);
assert_eq!(f.run(&[b"SINTERCARD", b"2", b"a", b"LIMIT"]), ":1\r\n");
}
#[test]
fn the_algebra_answers_wrongtype_before_it_writes_anything() {
let mut f = Fixture::new();
f.run(&[b"SADD", b"a", b"1"]);
f.run(&[b"SADD", b"d", b"old"]);
f.run(&[b"SET", b"str", b"v"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
for bad in [
&[b"SINTER".as_slice(), b"a", b"str"][..],
&[b"SUNION".as_slice(), b"str"][..],
&[b"SDIFF".as_slice(), b"a", b"str"][..],
&[b"SINTERCARD".as_slice(), b"2", b"a", b"str"][..],
&[b"SINTERSTORE".as_slice(), b"d", b"a", b"str"][..],
&[b"SUNIONSTORE".as_slice(), b"d", b"str"][..],
&[b"SDIFFSTORE".as_slice(), b"d", b"a", b"str"][..],
] {
let reply = f.run(bad);
assert_eq!(reply, wrong, "for {:?}", bad[0]);
}
assert_eq!(
f.run(&[b"SMEMBERS", b"d"]),
"*1\r\n$3\r\nold\r\n",
"and the destination was left alone every time"
);
}
#[test]
fn churning_sets_does_not_grow_the_server() {
let mut f = Fixture::new();
let members: Vec<Vec<u8>> = (0..200).map(|i| format!("m{i}").into_bytes()).collect();
let args: Vec<&[u8]> = std::iter::once(&b"SADD"[..])
.chain(std::iter::once(&b"s"[..]))
.chain(members.iter().map(Vec::as_slice))
.collect();
f.run(&args);
f.run(&[b"DEL", b"s"]);
f.server.compact_step();
let after_first = f.server.memory_bytes();
for _ in 0..200 {
f.run(&args);
f.run(&[b"DEL", b"s"]);
f.server.compact_step();
}
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
assert!(
f.server.memory_bytes() <= after_first * 2,
"held {} after two hundred passes against {after_first} after one",
f.server.memory_bytes()
);
}
fn bulks(parts: &[&str]) -> String {
let mut s = format!("*{}\r\n", parts.len());
for p in parts {
s.push_str(&format!("${}\r\n{p}\r\n", p.len()));
}
s
}
#[test]
fn a_list_is_pushed_from_both_ends_and_the_left_one_reverses() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"LPUSH", b"k", b"a", b"b", b"c"]), ":3\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"k", b"0", b"-1"]),
bulks(&["c", "b", "a"])
);
assert_eq!(f.run(&[b"RPUSH", b"k", b"d"]), ":4\r\n");
assert_eq!(f.run(&[b"LLEN", b"k"]), ":4\r\n");
assert_eq!(f.run(&[b"LPOP", b"k"]), "$1\r\nc\r\n");
assert_eq!(f.run(&[b"RPOP", b"k"]), "$1\r\nd\r\n");
assert_eq!(f.run(&[b"LRANGE", b"k", b"0", b"-1"]), bulks(&["b", "a"]));
assert_eq!(f.run(&[b"TYPE", b"k"]), "+list\r\n");
}
#[test]
fn the_x_pushes_refuse_to_bring_a_list_back_to_life() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"LPUSHX", b"k", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"RPUSHX", b"k", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"k"]), ":0\r\n");
f.run(&[b"RPUSH", b"k", b"a"]);
assert_eq!(f.run(&[b"LPUSHX", b"k", b"z"]), ":2\r\n");
assert_eq!(f.run(&[b"RPUSHX", b"k", b"y"]), ":3\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"k", b"0", b"-1"]),
bulks(&["z", "a", "y"])
);
}
#[test]
fn an_empty_pop_is_a_different_nothing_with_a_count_and_without() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"LPOP", b"nope"]), "$-1\r\n");
assert_eq!(f.run(&[b"LPOP", b"nope", b"2"]), "*-1\r\n");
assert_eq!(f.run(&[b"RPOP", b"nope"]), "$-1\r\n");
assert_eq!(f.run(&[b"RPOP", b"nope", b"2"]), "*-1\r\n");
f.run(&[b"RPUSH", b"k", b"a", b"b", b"c"]);
assert_eq!(f.run(&[b"LPOP", b"k", b"0"]), "*0\r\n");
assert_eq!(f.run(&[b"LPOP", b"k", b"1"]), bulks(&["a"]));
assert_eq!(f.run(&[b"RPOP", b"k", b"9"]), bulks(&["c", "b"]));
assert_eq!(f.run(&[b"EXISTS", b"k"]), ":0\r\n");
}
#[test]
fn a_pop_count_has_its_own_sentence_and_a_third_argument_is_an_arity_error() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"k", b"a"]);
let range = "-ERR value is out of range, must be positive\r\n";
assert_eq!(f.run(&[b"LPOP", b"k", b"-1"]), range);
assert_eq!(f.run(&[b"LPOP", b"k", b"abc"]), range);
assert_eq!(f.run(&[b"RPOP", b"k", b"-1"]), range);
assert_eq!(
f.run(&[b"LPOP", b"k", b"1", b"2"]),
"-ERR wrong number of arguments for 'lpop' command\r\n"
);
assert_eq!(f.run(&[b"LLEN", b"k"]), ":1\r\n");
}
#[test]
fn a_range_takes_negative_ends_and_clamps_the_ones_that_run_off() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"k", b"a", b"b", b"c"]);
assert_eq!(
f.run(&[b"LRANGE", b"k", b"0", b"-1"]),
bulks(&["a", "b", "c"])
);
assert_eq!(f.run(&[b"LRANGE", b"k", b"-2", b"-1"]), bulks(&["b", "c"]));
assert_eq!(f.run(&[b"LRANGE", b"k", b"1", b"1"]), bulks(&["b"]));
assert_eq!(f.run(&[b"LRANGE", b"k", b"5", b"10"]), "*0\r\n");
assert_eq!(f.run(&[b"LRANGE", b"k", b"2", b"1"]), "*0\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"k", b"-100", b"100"]),
bulks(&["a", "b", "c"])
);
assert_eq!(f.run(&[b"LRANGE", b"nope", b"0", b"-1"]), "*0\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"k", b"a", b"b"]),
"-ERR value is not an integer or out of range\r\n"
);
}
#[test]
fn an_index_reads_and_writes_from_whichever_end_is_nearer() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"k", b"a", b"b", b"c"]);
assert_eq!(f.run(&[b"LINDEX", b"k", b"0"]), "$1\r\na\r\n");
assert_eq!(f.run(&[b"LINDEX", b"k", b"-1"]), "$1\r\nc\r\n");
assert_eq!(f.run(&[b"LINDEX", b"k", b"99"]), "$-1\r\n");
assert_eq!(f.run(&[b"LINDEX", b"nope", b"0"]), "$-1\r\n");
assert_eq!(f.run(&[b"LSET", b"k", b"-1", b"z"]), "+OK\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"k", b"0", b"-1"]),
bulks(&["a", "b", "z"])
);
assert_eq!(
f.run(&[b"LSET", b"k", b"99", b"z"]),
"-ERR index out of range\r\n"
);
assert_eq!(
f.run(&[b"LSET", b"nope", b"0", b"z"]),
"-ERR no such key\r\n"
);
}
#[test]
fn linsert_says_three_things_with_one_signed_number() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"LINSERT", b"nope", b"BEFORE", b"a", b"x"]),
":0\r\n"
);
f.run(&[b"RPUSH", b"k", b"a", b"b"]);
assert_eq!(f.run(&[b"LINSERT", b"k", b"before", b"a", b"X"]), ":3\r\n");
assert_eq!(f.run(&[b"LINSERT", b"k", b"AFTER", b"b", b"Y"]), ":4\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"k", b"0", b"-1"]),
bulks(&["X", "a", "b", "Y"])
);
assert_eq!(
f.run(&[b"LINSERT", b"k", b"BEFORE", b"zz", b"x"]),
":-1\r\n"
);
assert_eq!(
f.run(&[b"LINSERT", b"k", b"SIDEWAYS", b"a", b"x"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn lrem_counts_in_three_directions_and_takes_the_key_when_it_empties() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"k", b"a", b"b", b"a", b"c", b"a"]);
assert_eq!(f.run(&[b"LREM", b"k", b"2", b"a"]), ":2\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"k", b"0", b"-1"]),
bulks(&["b", "c", "a"])
);
assert_eq!(f.run(&[b"LREM", b"k", b"-1", b"a"]), ":1\r\n");
assert_eq!(f.run(&[b"LRANGE", b"k", b"0", b"-1"]), bulks(&["b", "c"]));
assert_eq!(f.run(&[b"LREM", b"k", b"0", b"b"]), ":1\r\n");
assert_eq!(f.run(&[b"LREM", b"k", b"0", b"c"]), ":1\r\n");
assert_eq!(f.run(&[b"EXISTS", b"k"]), ":0\r\n");
assert_eq!(f.run(&[b"LREM", b"nope", b"0", b"a"]), ":0\r\n");
}
#[test]
fn ltrim_keeps_a_window_and_an_empty_one_deletes_the_key() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"k", b"a", b"b", b"c", b"d"]);
assert_eq!(f.run(&[b"LTRIM", b"k", b"1", b"-2"]), "+OK\r\n");
assert_eq!(f.run(&[b"LRANGE", b"k", b"0", b"-1"]), bulks(&["b", "c"]));
assert_eq!(f.run(&[b"LTRIM", b"k", b"1", b"0"]), "+OK\r\n");
assert_eq!(f.run(&[b"EXISTS", b"k"]), ":0\r\n");
assert_eq!(f.run(&[b"LTRIM", b"nope", b"0", b"-1"]), "+OK\r\n");
}
#[test]
fn lpos_walks_from_either_end_and_stops_where_it_is_told() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"p", b"a", b"b", b"c", b"a", b"b", b"c", b"a"]);
assert_eq!(f.run(&[b"LPOS", b"p", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"LPOS", b"p", b"a", b"RANK", b"-1"]), ":6\r\n");
assert_eq!(f.run(&[b"LPOS", b"p", b"a", b"RANK", b"2"]), ":3\r\n");
assert_eq!(
f.run(&[b"LPOS", b"p", b"a", b"COUNT", b"2"]),
"*2\r\n:0\r\n:3\r\n"
);
assert_eq!(
f.run(&[b"LPOS", b"p", b"a", b"RANK", b"-1", b"COUNT", b"0"]),
"*3\r\n:6\r\n:3\r\n:0\r\n"
);
assert_eq!(
f.run(&[b"LPOS", b"p", b"a", b"COUNT", b"0", b"MAXLEN", b"3"]),
"*1\r\n:0\r\n"
);
assert_eq!(f.run(&[b"LPOS", b"p", b"zz"]), "$-1\r\n");
assert_eq!(f.run(&[b"LPOS", b"p", b"zz", b"COUNT", b"0"]), "*0\r\n");
assert_eq!(f.run(&[b"LPOS", b"nope", b"a"]), "$-1\r\n");
assert_eq!(f.run(&[b"LPOS", b"nope", b"a", b"COUNT", b"2"]), "*0\r\n");
}
#[test]
fn lpos_words_its_three_mistakes_the_way_redis_does() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"p", b"a"]);
assert_eq!(
f.run(&[b"LPOS", b"p", b"a", b"RANK", b"0"]),
"-ERR RANK can't be zero: use 1 to start from the first match, 2 from the second ... or use negative to start from the end of the list\r\n"
);
assert_eq!(
f.run(&[b"LPOS", b"p", b"a", b"COUNT", b"-1"]),
"-ERR COUNT can't be negative\r\n"
);
assert_eq!(
f.run(&[b"LPOS", b"p", b"a", b"MAXLEN", b"-1"]),
"-ERR MAXLEN can't be negative\r\n"
);
assert_eq!(
f.run(&[b"LPOS", b"p", b"a", b"RANK"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"LPOS", b"p", b"a", b"FOO", b"1"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_move_takes_from_one_end_and_gives_to_another_even_on_one_key() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"k", b"a", b"b", b"c"]);
assert_eq!(f.run(&[b"RPOPLPUSH", b"k", b"d"]), "$1\r\nc\r\n");
assert_eq!(f.run(&[b"LRANGE", b"k", b"0", b"-1"]), bulks(&["a", "b"]));
assert_eq!(f.run(&[b"LRANGE", b"d", b"0", b"-1"]), bulks(&["c"]));
assert_eq!(
f.run(&[b"LMOVE", b"k", b"d", b"LEFT", b"RIGHT"]),
"$1\r\na\r\n"
);
assert_eq!(f.run(&[b"LRANGE", b"d", b"0", b"-1"]), bulks(&["c", "a"]));
f.run(&[b"DEL", b"r"]);
f.run(&[b"RPUSH", b"r", b"1", b"2", b"3"]);
assert_eq!(f.run(&[b"RPOPLPUSH", b"r", b"r"]), "$1\r\n3\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"r", b"0", b"-1"]),
bulks(&["3", "1", "2"])
);
assert_eq!(
f.run(&[b"LMOVE", b"nope", b"d", b"LEFT", b"LEFT"]),
"$-1\r\n"
);
assert_eq!(
f.run(&[b"LMOVE", b"r", b"d", b"LEFT", b"SIDEWAYS"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_move_checks_the_destination_before_it_takes_anything() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"k", b"a", b"b"]);
f.run(&[b"SET", b"str", b"v"]);
assert_eq!(
f.run(&[b"LMOVE", b"k", b"str", b"LEFT", b"LEFT"]),
"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"
);
assert_eq!(f.run(&[b"LRANGE", b"k", b"0", b"-1"]), bulks(&["a", "b"]));
}
#[test]
fn lmpop_answers_from_the_first_key_that_has_anything() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"b", b"1", b"2", b"3"]);
assert_eq!(
f.run(&[b"LMPOP", b"2", b"a", b"b", b"LEFT", b"COUNT", b"2"]),
"*2\r\n$1\r\nb\r\n*2\r\n$1\r\n1\r\n$1\r\n2\r\n"
);
assert_eq!(
f.run(&[b"LMPOP", b"2", b"a", b"b", b"RIGHT"]),
"*2\r\n$1\r\nb\r\n*1\r\n$1\r\n3\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"b"]), ":0\r\n");
assert_eq!(f.run(&[b"LMPOP", b"2", b"a", b"b", b"LEFT"]), "*-1\r\n");
}
#[test]
fn lmpop_has_its_own_words_for_a_count_and_for_a_key_count() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"k", b"a"]);
assert_eq!(
f.run(&[b"LMPOP", b"0", b"k", b"LEFT"]),
"-ERR numkeys should be greater than 0\r\n"
);
assert_eq!(
f.run(&[b"LMPOP", b"-1", b"k", b"LEFT"]),
"-ERR numkeys should be greater than 0\r\n"
);
assert_eq!(
f.run(&[b"LMPOP", b"1", b"k", b"LEFT", b"COUNT", b"0"]),
"-ERR count should be greater than 0\r\n"
);
assert_eq!(
f.run(&[b"LMPOP", b"3", b"k", b"LEFT"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"LMPOP", b"1", b"k", b"LEFT", b"COUNT", b"1", b"x"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"LMPOP", b"1", b"k", b"LEFT", b"FOO", b"1"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"LMPOP", b"1", b"k", b"SIDEWAYS"]),
"-ERR syntax error\r\n"
);
assert_eq!(f.run(&[b"LLEN", b"k"]), ":1\r\n");
}
#[test]
fn every_list_command_says_wrongtype_and_writes_nothing() {
let mut f = Fixture::new();
f.run(&[b"SET", b"str", b"v"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
for cmd in [
&[b"LPUSH".as_slice(), b"str", b"a"][..],
&[b"RPUSH", b"str", b"a"],
&[b"LPUSHX", b"str", b"a"],
&[b"RPUSHX", b"str", b"a"],
&[b"LPOP", b"str"],
&[b"LPOP", b"str", b"2"],
&[b"RPOP", b"str"],
&[b"LLEN", b"str"],
&[b"LRANGE", b"str", b"0", b"-1"],
&[b"LINDEX", b"str", b"0"],
&[b"LSET", b"str", b"0", b"a"],
&[b"LINSERT", b"str", b"BEFORE", b"a", b"b"],
&[b"LREM", b"str", b"0", b"a"],
&[b"LTRIM", b"str", b"0", b"-1"],
&[b"LPOS", b"str", b"a"],
&[b"LPOS", b"str", b"a", b"COUNT", b"0"],
&[b"RPOPLPUSH", b"str", b"d"],
&[b"LMOVE", b"str", b"d", b"LEFT", b"LEFT"],
&[b"LMPOP", b"1", b"str", b"LEFT"],
] {
assert_eq!(f.run(cmd), wrong, "{:?}", String::from_utf8_lossy(cmd[0]));
}
assert_eq!(f.run(&[b"GET", b"str"]), "$1\r\nv\r\n");
assert_eq!(f.run(&[b"EXISTS", b"d"]), ":0\r\n");
}
#[test]
fn a_timeout_has_three_ways_of_being_wrong() {
let mut f = Fixture::new();
let not_float = "-ERR timeout is not a float or out of range\r\n";
let range = "-ERR timeout is out of range\r\n";
for (bad, want) in [
(&[b"BLPOP".as_slice(), b"k", b"abc"][..], not_float),
(&[b"BLPOP", b"k", b"nan"], not_float),
(&[b"BLPOP", b"k", b""], not_float),
(&[b"BLPOP", b"k", b" 1"], not_float),
(&[b"BLPOP", b"k", b"1 "], not_float),
(&[b"BLPOP", b"k", b"-1"], "-ERR timeout is negative\r\n"),
(&[b"BLPOP", b"k", b"-0.1"], "-ERR timeout is negative\r\n"),
(&[b"BLPOP", b"k", b"1e400"], range),
(&[b"BLPOP", b"k", b"inf"], range),
(&[b"BLPOP", b"k", b"9999999999999999"], range),
(&[b"BRPOP", b"k", b"abc"], not_float),
(
&[b"BLMOVE", b"a", b"b", b"LEFT", b"RIGHT", b"abc"],
not_float,
),
(
&[b"BRPOPLPUSH", b"a", b"b", b"-1"],
"-ERR timeout is negative\r\n",
),
(&[b"BLMPOP", b"abc", b"1", b"k", b"LEFT"], not_float),
] {
assert_eq!(f.run(bad), want, "for {bad:?}");
}
}
#[test]
fn a_zero_timeout_waits_and_the_smallest_positive_one_does_not() {
let mut f = Fixture::new();
for timeout in [b"0".as_slice(), b"0.0", b"-0.0"] {
let (flow, out) = f.flow(&[b"BLPOP", b"k", timeout]);
assert_eq!(flow, Flow::Block, "for {timeout:?}");
assert!(out.is_empty(), "for {timeout:?}");
}
let (flow, out) = f.flow(&[b"BLPOP", b"k", b"0.0000001"]);
assert_eq!(flow, Flow::Block);
assert!(out.is_empty());
}
#[test]
fn a_blocking_command_that_can_be_answered_answers_like_the_one_it_wraps() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"L", b"a", b"b", b"c", b"d", b"e"]);
assert_eq!(
f.flow(&[b"BLPOP", b"nope", b"L", b"0"]),
(Flow::Continue, "*2\r\n$1\r\nL\r\n$1\r\na\r\n".to_owned())
);
assert_eq!(
f.run(&[b"BRPOP", b"L", b"0"]),
"*2\r\n$1\r\nL\r\n$1\r\ne\r\n"
);
assert_eq!(
f.run(&[
b"BLMPOP", b"0", b"2", b"nope", b"L", b"LEFT", b"COUNT", b"2"
]),
"*2\r\n$1\r\nL\r\n*2\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
assert_eq!(
f.run(&[b"BLMOVE", b"L", b"D", b"LEFT", b"RIGHT", b"0"]),
"$1\r\nd\r\n"
);
assert_eq!(
f.run(&[b"EXISTS", b"L"]),
":0\r\n",
"and the key went with it"
);
assert_eq!(f.run(&[b"LRANGE", b"D", b"0", b"-1"]), "*1\r\n$1\r\nd\r\n");
f.run(&[b"RPUSH", b"D", b"x"]);
assert_eq!(f.run(&[b"BRPOPLPUSH", b"D", b"D", b"0"]), "$1\r\nx\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"D", b"0", b"-1"]),
"*2\r\n$1\r\nx\r\n$1\r\nd\r\n"
);
}
#[test]
fn blmpop_reads_its_count_and_its_key_count_the_way_lmpop_does() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"k", b"a"]);
for (bad, want) in [
(
&[b"BLMPOP".as_slice(), b"0", b"0", b"k", b"LEFT"][..],
"-ERR numkeys should be greater than 0\r\n",
),
(
&[b"BLMPOP", b"0", b"-1", b"k", b"LEFT"],
"-ERR numkeys should be greater than 0\r\n",
),
(
&[b"BLMPOP", b"0", b"2", b"k", b"LEFT"],
"-ERR syntax error\r\n",
),
(
&[b"BLMPOP", b"0", b"1", b"k", b"SIDEWAYS"],
"-ERR syntax error\r\n",
),
(
&[b"BLMPOP", b"0", b"1", b"k", b"LEFT", b"COUNT"],
"-ERR syntax error\r\n",
),
(
&[b"BLMPOP", b"0", b"1", b"k", b"LEFT", b"COUNT", b"2", b"x"],
"-ERR syntax error\r\n",
),
(
&[b"BLMPOP", b"0", b"1", b"k", b"LEFT", b"COUNT", b"0"],
"-ERR count should be greater than 0\r\n",
),
(
&[b"BLMPOP", b"0", b"1", b"k", b"LEFT", b"COUNT", b"abc"],
"-ERR count should be greater than 0\r\n",
),
] {
assert_eq!(f.run(bad), want, "for {bad:?}");
}
assert_eq!(f.run(&[b"LLEN", b"k"]), ":1\r\n", "and none of them popped");
}
#[test]
fn a_blocking_move_reads_its_directions_before_its_timeout() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"BLMOVE", b"a", b"b", b"UP", b"DOWN", b"abc"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"BLMOVE", b"a", b"b", b"LEFT", b"DOWN", b"0.05"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_blocking_command_errors_on_a_wrong_type_rather_than_waiting_on_it() {
let mut f = Fixture::new();
f.run(&[b"SET", b"S", b"v"]);
f.run(&[b"RPUSH", b"D", b"x"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
assert_eq!(f.run(&[b"BLPOP", b"S", b"0"]), wrong);
assert_eq!(f.run(&[b"BLPOP", b"E", b"S", b"0"]), wrong);
assert_eq!(f.run(&[b"BRPOP", b"S", b"0"]), wrong);
assert_eq!(f.run(&[b"BLMPOP", b"0", b"1", b"S", b"LEFT"]), wrong);
assert_eq!(f.run(&[b"BRPOPLPUSH", b"S", b"D", b"0"]), wrong);
assert_eq!(f.run(&[b"BRPOPLPUSH", b"D", b"S", b"0"]), wrong);
assert_eq!(f.run(&[b"LRANGE", b"D", b"0", b"-1"]), "*1\r\n$1\r\nx\r\n");
assert_eq!(
f.flow(&[b"BLMOVE", b"E", b"S", b"LEFT", b"RIGHT", b"0.1"])
.0,
Flow::Block
);
}
#[test]
fn churning_lists_does_not_grow_the_server() {
let mut f = Fixture::new();
let vals: Vec<Vec<u8>> = (0..200).map(|i| format!("v{i}").into_bytes()).collect();
let args: Vec<&[u8]> = [&b"RPUSH"[..], &b"k"[..]]
.into_iter()
.chain(vals.iter().map(Vec::as_slice))
.collect();
f.run(&args);
f.run(&[b"DEL", b"k"]);
f.server.compact_step();
let after_first = f.server.memory_bytes();
for _ in 0..200 {
f.run(&args);
f.run(&[b"LTRIM", b"k", b"1", b"0"]);
f.server.compact_step();
}
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
assert!(
f.server.memory_bytes() <= after_first * 2,
"held {} after two hundred passes against {after_first} after one",
f.server.memory_bytes()
);
}
#[test]
fn a_sorted_set_takes_scores_and_gives_them_back() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b"]), ":2\r\n");
assert_eq!(f.run(&[b"ZADD", b"z", b"1", b"a", b"3", b"c"]), ":1\r\n");
assert_eq!(f.run(&[b"ZCARD", b"z"]), ":3\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z", b"b"]), "$1\r\n2\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z", b"nope"]), "$-1\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"nokey", b"b"]), "$-1\r\n");
assert_eq!(
f.run(&[b"ZMSCORE", b"z", b"a", b"nope", b"c"]),
"*3\r\n$1\r\n1\r\n$-1\r\n$1\r\n3\r\n"
);
assert_eq!(f.run(&[b"ZREM", b"z", b"a", b"nope"]), ":1\r\n");
assert_eq!(f.run(&[b"ZCARD", b"z"]), ":2\r\n");
assert_eq!(f.run(&[b"ZREM", b"z", b"b", b"c"]), ":2\r\n");
assert_eq!(f.run(&[b"EXISTS", b"z"]), ":0\r\n");
}
#[test]
fn a_score_is_a_double_on_resp3_and_digits_on_resp2() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1.5", b"a", b"inf", b"b", b"-inf", b"c"]);
assert_eq!(f.run(&[b"ZSCORE", b"z", b"a"]), "$3\r\n1.5\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z", b"b"]), "$3\r\ninf\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z", b"c"]), "$4\r\n-inf\r\n");
f.out = Out::new(Proto::Resp3);
assert_eq!(f.run(&[b"ZSCORE", b"z", b"a"]), ",1.5\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z", b"b"]), ",inf\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z", b"c"]), ",-inf\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z", b"nope"]), "_\r\n");
}
#[test]
fn the_zadd_options_gate_what_gets_written() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"5", b"a"]);
assert_eq!(f.run(&[b"ZADD", b"z", b"NX", b"9", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z", b"a"]), "$1\r\n5\r\n");
assert_eq!(f.run(&[b"ZADD", b"z", b"XX", b"9", b"new"]), ":0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"z"]), ":1\r\n");
assert_eq!(f.run(&[b"ZADD", b"z", b"GT", b"CH", b"3", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"ZADD", b"z", b"GT", b"CH", b"7", b"a"]), ":1\r\n");
assert_eq!(f.run(&[b"ZADD", b"z", b"LT", b"CH", b"9", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"ZADD", b"z", b"1", b"a", b"1", b"b"]), ":1\r\n");
assert_eq!(
f.run(&[b"ZADD", b"z", b"CH", b"2", b"a", b"2", b"c"]),
":2\r\n"
);
}
#[test]
fn zadd_incr_answers_a_score_or_nothing_at_all() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"ZADD", b"z", b"INCR", b"5", b"m"]), "$1\r\n5\r\n");
assert_eq!(f.run(&[b"ZADD", b"z", b"INCR", b"2", b"m"]), "$1\r\n7\r\n");
assert_eq!(
f.run(&[b"ZADD", b"z", b"NX", b"INCR", b"2", b"m"]),
"$-1\r\n"
);
assert_eq!(
f.run(&[b"ZADD", b"z", b"XX", b"INCR", b"2", b"gone"]),
"$-1\r\n"
);
assert_eq!(
f.run(&[b"ZADD", b"z", b"GT", b"INCR", b"-1", b"m"]),
"$-1\r\n"
);
assert_eq!(
f.run(&[b"ZADD", b"z", b"GT", b"INCR", b"1", b"m"]),
"$1\r\n8\r\n"
);
assert_eq!(f.run(&[b"ZINCRBY", b"z", b"2", b"m"]), "$2\r\n10\r\n");
assert_eq!(f.run(&[b"ZINCRBY", b"z", b"1", b"fresh"]), "$1\r\n1\r\n");
}
#[test]
fn the_two_infinities_will_not_be_added_together() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"inf", b"m"]);
let nan = "-ERR resulting score is not a number (NaN)\r\n";
assert_eq!(f.run(&[b"ZINCRBY", b"z", b"-inf", b"m"]), nan);
assert_eq!(f.run(&[b"ZADD", b"z", b"INCR", b"-inf", b"m"]), nan);
assert_eq!(f.run(&[b"ZSCORE", b"z", b"m"]), "$3\r\ninf\r\n");
assert_eq!(f.run(&[b"ZINCRBY", b"gone", b"1", b"m"]), "$1\r\n1\r\n");
}
#[test]
fn zadd_says_its_mistakes_the_way_redis_says_them() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"ZADD", b"z", b"NX", b"XX"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"ZADD", b"z", b"NX", b"XX", b"1", b"a"]),
"-ERR XX and NX options at the same time are not compatible\r\n"
);
let gtlt = "-ERR GT, LT, and/or NX options at the same time are not compatible\r\n";
assert_eq!(f.run(&[b"ZADD", b"z", b"NX", b"GT", b"1", b"a"]), gtlt);
assert_eq!(f.run(&[b"ZADD", b"z", b"GT", b"LT", b"1", b"a"]), gtlt);
assert_eq!(
f.run(&[b"ZADD", b"z", b"INCR", b"1", b"a", b"2", b"b"]),
"-ERR INCR option supports a single increment-element pair\r\n"
);
assert_eq!(
f.run(&[b"ZADD", b"z", b"1", b"a", b"2"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"ZADD", b"z", b"1", b"a", b"nonsense", b"b"]),
"-ERR value is not a valid float\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"z"]), ":0\r\n");
}
#[test]
fn a_rank_says_where_a_member_sits_from_either_end() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(f.run(&[b"ZRANK", b"z", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"ZRANK", b"z", b"c"]), ":2\r\n");
assert_eq!(f.run(&[b"ZREVRANK", b"z", b"c"]), ":0\r\n");
assert_eq!(f.run(&[b"ZREVRANK", b"z", b"a"]), ":2\r\n");
assert_eq!(
f.run(&[b"ZRANK", b"z", b"b", b"WITHSCORE"]),
"*2\r\n:1\r\n$1\r\n2\r\n"
);
assert_eq!(f.run(&[b"ZRANK", b"z", b"nope"]), "$-1\r\n");
assert_eq!(f.run(&[b"ZRANK", b"z", b"nope", b"WITHSCORE"]), "*-1\r\n");
assert_eq!(f.run(&[b"ZRANK", b"nokey", b"a", b"WITHSCORE"]), "*-1\r\n");
assert_eq!(
f.run(&[b"ZRANK", b"z", b"b", b"bogus"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"ZREVRANK", b"z", b"b", b"WITHSCORE", b"more"]),
"-ERR wrong number of arguments for 'zrevrank' command\r\n"
);
}
#[test]
fn the_two_counts_read_their_two_kinds_of_bound() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(f.run(&[b"ZCOUNT", b"z", b"-inf", b"+inf"]), ":3\r\n");
assert_eq!(f.run(&[b"ZCOUNT", b"z", b"2", b"3"]), ":2\r\n");
assert_eq!(f.run(&[b"ZCOUNT", b"z", b"(1", b"3"]), ":2\r\n");
assert_eq!(f.run(&[b"ZCOUNT", b"z", b"(1", b"(3"]), ":1\r\n");
assert_eq!(f.run(&[b"ZCOUNT", b"nokey", b"-inf", b"+inf"]), ":0\r\n");
assert_eq!(
f.run(&[b"ZCOUNT", b"z", b"bogus", b"3"]),
"-ERR min or max is not a float\r\n"
);
f.run(&[b"ZADD", b"l", b"0", b"a", b"0", b"b", b"0", b"c"]);
assert_eq!(f.run(&[b"ZLEXCOUNT", b"l", b"-", b"+"]), ":3\r\n");
assert_eq!(f.run(&[b"ZLEXCOUNT", b"l", b"[a", b"(c"]), ":2\r\n");
assert_eq!(f.run(&[b"ZLEXCOUNT", b"l", b"(a", b"+"]), ":2\r\n");
assert_eq!(
f.run(&[b"ZLEXCOUNT", b"l", b"a", b"c"]),
"-ERR min or max not valid string range item\r\n"
);
}
#[test]
fn one_range_command_selects_by_rank_or_score_or_name() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"0", b"-1"]),
"*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"-2", b"-1"]),
"*2\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
assert_eq!(f.run(&[b"ZRANGE", b"z", b"5", b"9"]), "*0\r\n");
assert_eq!(f.run(&[b"ZRANGE", b"nokey", b"0", b"-1"]), "*0\r\n");
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"0", b"-1", b"REV"]),
"*3\r\n$1\r\nc\r\n$1\r\nb\r\n$1\r\na\r\n"
);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"(1", b"+inf", b"BYSCORE"]),
"*2\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"+inf", b"(1", b"BYSCORE", b"REV"]),
"*2\r\n$1\r\nc\r\n$1\r\nb\r\n"
);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"-", b"+", b"BYLEX"]),
"*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"+", b"-", b"BYLEX", b"REV"]),
"*3\r\n$1\r\nc\r\n$1\r\nb\r\n$1\r\na\r\n"
);
}
#[test]
fn the_older_range_spellings_name_their_high_end_first() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(
f.run(&[b"ZREVRANGE", b"z", b"0", b"-1"]),
"*3\r\n$1\r\nc\r\n$1\r\nb\r\n$1\r\na\r\n"
);
assert_eq!(
f.run(&[b"ZREVRANGE", b"z", b"0", b"0", b"WITHSCORES"]),
"*2\r\n$1\r\nc\r\n$1\r\n3\r\n"
);
assert_eq!(
f.run(&[b"ZRANGEBYSCORE", b"z", b"(1", b"3"]),
"*2\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
assert_eq!(
f.run(&[b"ZREVRANGEBYSCORE", b"z", b"3", b"(1"]),
"*2\r\n$1\r\nc\r\n$1\r\nb\r\n"
);
assert_eq!(f.run(&[b"ZREVRANGEBYSCORE", b"z", b"(1", b"3"]), "*0\r\n");
assert_eq!(
f.run(&[b"ZRANGEBYLEX", b"z", b"[a", b"(c"]),
"*2\r\n$1\r\na\r\n$1\r\nb\r\n"
);
assert_eq!(
f.run(&[b"ZREVRANGEBYLEX", b"z", b"(c", b"[a"]),
"*2\r\n$1\r\nb\r\n$1\r\na\r\n"
);
for cmd in [
&[b"ZREVRANGE".as_slice(), b"z", b"0", b"-1", b"BYSCORE"][..],
&[b"ZRANGEBYSCORE", b"z", b"1", b"3", b"REV"],
&[b"ZRANGEBYLEX", b"z", b"[a", b"[c", b"BYLEX"],
] {
assert_eq!(f.run(cmd), "-ERR syntax error\r\n", "{:?}", cmd[0]);
}
}
#[test]
fn limit_and_withscores_are_read_by_all_of_them_and_refused_afterwards() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(
f.run(&[
b"ZRANGE", b"z", b"-inf", b"+inf", b"BYSCORE", b"LIMIT", b"1", b"1"
]),
"*1\r\n$1\r\nb\r\n"
);
assert_eq!(
f.run(&[
b"ZRANGE", b"z", b"-inf", b"+inf", b"BYSCORE", b"LIMIT", b"-1", b"2"
]),
"*0\r\n"
);
assert_eq!(
f.run(&[
b"ZRANGE", b"z", b"-inf", b"+inf", b"BYSCORE", b"LIMIT", b"0", b"-1"
]),
"*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
let both = "*4\r\n$1\r\na\r\n$1\r\n1\r\n$1\r\nb\r\n$1\r\n2\r\n";
assert_eq!(
f.run(&[
b"ZRANGEBYSCORE",
b"z",
b"1",
b"3",
b"WITHSCORES",
b"LIMIT",
b"0",
b"2"
]),
both
);
assert_eq!(
f.run(&[
b"ZRANGEBYSCORE",
b"z",
b"1",
b"3",
b"LIMIT",
b"0",
b"2",
b"WITHSCORES"
]),
both
);
let needs_by = "-ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX\r\n";
assert_eq!(
f.run(&[
b"ZREVRANGE",
b"z",
b"0",
b"-1",
b"WITHSCORES",
b"LIMIT",
b"0",
b"1"
]),
needs_by
);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"0", b"-1", b"LIMIT", b"0", b"1"]),
needs_by
);
let not_bylex = "-ERR syntax error, WITHSCORES not supported in combination with BYLEX\r\n";
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"-", b"+", b"BYLEX", b"WITHSCORES"]),
not_bylex
);
assert_eq!(
f.run(&[b"ZRANGEBYLEX", b"z", b"[a", b"[c", b"WITHSCORES"]),
not_bylex
);
for cmd in [
&[
b"ZRANGE".as_slice(),
b"z",
b"0",
b"-1",
b"BYSCORE",
b"BYLEX",
][..],
&[b"ZRANGE", b"z", b"0", b"-1", b"junk"],
&[b"ZRANGEBYSCORE", b"z", b"1", b"3", b"LIMIT", b"0"],
] {
assert_eq!(f.run(cmd), "-ERR syntax error\r\n", "{cmd:?}");
}
assert_eq!(
f.run(&[b"ZRANGEBYSCORE", b"z", b"bad", b"3"]),
"-ERR min or max is not a float\r\n"
);
assert_eq!(
f.run(&[b"ZRANGEBYLEX", b"z", b"a", b"[c"]),
"-ERR min or max not valid string range item\r\n"
);
assert_eq!(
f.run(&[b"ZRANGEBYSCORE", b"z", b"1", b"3", b"LIMIT", b"a", b"2"]),
"-ERR value is not an integer or out of range\r\n"
);
}
#[test]
fn withscores_nests_on_resp3_and_flattens_on_resp2() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"0", b"-1", b"WITHSCORES"]),
"*6\r\n$1\r\na\r\n$1\r\n1\r\n$1\r\nb\r\n$1\r\n2\r\n$1\r\nc\r\n$1\r\n3\r\n"
);
f.out = Out::new(Proto::Resp3);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"0", b"-1", b"WITHSCORES"]),
"*3\r\n*2\r\n$1\r\na\r\n,1\r\n*2\r\n$1\r\nb\r\n,2\r\n*2\r\n$1\r\nc\r\n,3\r\n"
);
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"0", b"-1"]),
"*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
}
#[test]
fn a_range_store_writes_the_window_into_another_key() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(f.run(&[b"ZRANGESTORE", b"d", b"z", b"0", b"-1"]), ":3\r\n");
assert_eq!(f.run(&[b"ZRANGESTORE", b"d", b"z", b"5", b"9"]), ":0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"d"]), ":0\r\n");
assert_eq!(
f.run(&[b"ZRANGESTORE", b"d", b"z", b"(1", b"+inf", b"BYSCORE"]),
":2\r\n"
);
assert_eq!(
f.run(&[b"ZRANGE", b"d", b"0", b"-1", b"WITHSCORES"]),
"*4\r\n$1\r\nb\r\n$1\r\n2\r\n$1\r\nc\r\n$1\r\n3\r\n"
);
assert_eq!(f.run(&[b"ZRANGESTORE", b"z", b"z", b"1", b"2"]), ":2\r\n");
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"0", b"-1", b"WITHSCORES"]),
"*4\r\n$1\r\nb\r\n$1\r\n2\r\n$1\r\nc\r\n$1\r\n3\r\n"
);
assert_eq!(
f.run(&[b"ZRANGESTORE", b"d", b"z", b"0", b"-1", b"WITHSCORES"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn the_three_removals_share_their_window_with_the_reads() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(f.run(&[b"ZREMRANGEBYRANK", b"z", b"0", b"0"]), ":1\r\n");
assert_eq!(
f.run(&[b"ZRANGE", b"z", b"0", b"-1"]),
"*2\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
assert_eq!(
f.run(&[b"ZREMRANGEBYSCORE", b"z", b"(2", b"+inf"]),
":1\r\n"
);
assert_eq!(f.run(&[b"ZRANGE", b"z", b"0", b"-1"]), "*1\r\n$1\r\nb\r\n");
assert_eq!(f.run(&[b"ZREMRANGEBYLEX", b"z", b"-", b"+"]), ":1\r\n");
assert_eq!(f.run(&[b"EXISTS", b"z"]), ":0\r\n");
assert_eq!(
f.run(&[b"ZREMRANGEBYRANK", b"nokey", b"0", b"-1"]),
":0\r\n"
);
assert_eq!(
f.run(&[b"ZREMRANGEBYRANK", b"z", b"0", b"x"]),
"-ERR value is not an integer or out of range\r\n"
);
}
#[test]
fn the_three_algebra_commands_combine_scores_and_order_the_answer_once() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
f.run(&[b"ZADD", b"y", b"10", b"b", b"20", b"d"]);
assert_eq!(
f.run(&[b"ZUNION", b"2", b"z", b"y"]),
"*4\r\n$1\r\na\r\n$1\r\nc\r\n$1\r\nb\r\n$1\r\nd\r\n"
);
assert_eq!(
f.run(&[b"ZUNION", b"2", b"z", b"y", b"WITHSCORES"]),
"*8\r\n$1\r\na\r\n$1\r\n1\r\n$1\r\nc\r\n$1\r\n3\r\n$1\r\nb\r\n$2\r\n12\r\n$1\r\nd\r\n$2\r\n20\r\n"
);
assert_eq!(
f.run(&[
b"ZUNION",
b"2",
b"z",
b"y",
b"WEIGHTS",
b"2",
b"3",
b"WITHSCORES"
]),
"*8\r\n$1\r\na\r\n$1\r\n2\r\n$1\r\nc\r\n$1\r\n6\r\n$1\r\nb\r\n$2\r\n34\r\n$1\r\nd\r\n$2\r\n60\r\n"
);
assert_eq!(
f.run(&[
b"ZUNION",
b"2",
b"z",
b"y",
b"AGGREGATE",
b"MIN",
b"WITHSCORES"
]),
"*8\r\n$1\r\na\r\n$1\r\n1\r\n$1\r\nb\r\n$1\r\n2\r\n$1\r\nc\r\n$1\r\n3\r\n$1\r\nd\r\n$2\r\n20\r\n"
);
assert_eq!(
f.run(&[
b"ZUNION",
b"2",
b"z",
b"y",
b"AGGREGATE",
b"MAX",
b"WITHSCORES"
]),
"*8\r\n$1\r\na\r\n$1\r\n1\r\n$1\r\nc\r\n$1\r\n3\r\n$1\r\nb\r\n$2\r\n10\r\n$1\r\nd\r\n$2\r\n20\r\n"
);
assert_eq!(
f.run(&[b"ZINTER", b"2", b"z", b"y", b"WITHSCORES"]),
"*2\r\n$1\r\nb\r\n$2\r\n12\r\n"
);
assert_eq!(
f.run(&[b"ZDIFF", b"2", b"z", b"y", b"WITHSCORES"]),
"*4\r\n$1\r\na\r\n$1\r\n1\r\n$1\r\nc\r\n$1\r\n3\r\n"
);
assert_eq!(f.run(&[b"ZUNION", b"1", b"nokey"]), "*0\r\n");
f.run(&[b"SADD", b"p", b"a", b"d"]);
assert_eq!(
f.run(&[b"ZUNION", b"2", b"z", b"p", b"WITHSCORES"]),
"*8\r\n$1\r\nd\r\n$1\r\n1\r\n$1\r\na\r\n$1\r\n2\r\n$1\r\nb\r\n$1\r\n2\r\n$1\r\nc\r\n$1\r\n3\r\n"
);
for cmd in [
&[
b"ZDIFF".as_slice(),
b"2",
b"z",
b"y",
b"WEIGHTS",
b"1",
b"1",
][..],
&[b"ZDIFF", b"2", b"z", b"y", b"AGGREGATE", b"MIN"],
] {
assert_eq!(f.run(cmd), "-ERR syntax error\r\n", "{cmd:?}");
}
}
#[test]
fn the_algebra_counts_its_keys_and_says_so_when_the_count_is_wrong() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a"]);
f.run(&[b"ZADD", b"y", b"2", b"b"]);
assert_eq!(
f.run(&[b"ZUNION", b"0", b"z"]),
"-ERR at least 1 input key is needed for 'zunion' command\r\n"
);
assert_eq!(
f.run(&[b"ZUNION", b"-1", b"z"]),
"-ERR at least 1 input key is needed for 'zunion' command\r\n"
);
assert_eq!(
f.run(&[b"ZINTERCARD", b"0", b"z"]),
"-ERR at least 1 input key is needed for 'zintercard' command\r\n"
);
assert_eq!(
f.run(&[b"ZUNION", b"3", b"z", b"y"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"ZUNION", b"x", b"z"]),
"-ERR value is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"ZUNION", b"2", b"z", b"y", b"WEIGHTS", b"1"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"ZUNION", b"2", b"z", b"y", b"WEIGHTS", b"a", b"b"]),
"-ERR weight value is not a float\r\n"
);
assert_eq!(
f.run(&[b"ZUNION", b"2", b"z", b"y", b"AGGREGATE", b"NOPE"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn the_algebra_stores_answer_a_count_and_delete_an_empty_destination() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
f.run(&[b"ZADD", b"y", b"10", b"b", b"20", b"d"]);
assert_eq!(f.run(&[b"ZUNIONSTORE", b"d", b"2", b"z", b"y"]), ":4\r\n");
assert_eq!(
f.run(&[b"ZRANGE", b"d", b"0", b"-1", b"WITHSCORES"]),
"*8\r\n$1\r\na\r\n$1\r\n1\r\n$1\r\nc\r\n$1\r\n3\r\n$1\r\nb\r\n$2\r\n12\r\n$1\r\nd\r\n$2\r\n20\r\n"
);
assert_eq!(f.run(&[b"ZINTERSTORE", b"d", b"2", b"z", b"y"]), ":1\r\n");
assert_eq!(f.run(&[b"ZDIFFSTORE", b"d", b"2", b"z", b"y"]), ":2\r\n");
assert_eq!(
f.run(&[b"ZINTERSTORE", b"d", b"2", b"z", b"nokey"]),
":0\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"d"]), ":0\r\n");
assert_eq!(f.run(&[b"ZUNIONSTORE", b"z", b"2", b"z", b"y"]), ":4\r\n");
assert_eq!(f.run(&[b"ZCARD", b"z"]), ":4\r\n");
for cmd in [
&[
b"ZUNIONSTORE".as_slice(),
b"d",
b"2",
b"z",
b"y",
b"WITHSCORES",
][..],
&[
b"ZDIFFSTORE",
b"d",
b"2",
b"z",
b"y",
b"WEIGHTS",
b"1",
b"1",
],
] {
assert_eq!(f.run(cmd), "-ERR syntax error\r\n", "{cmd:?}");
}
}
#[test]
fn intercard_counts_and_stops_at_its_limit() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
f.run(&[b"ZADD", b"y", b"10", b"b", b"20", b"c", b"30", b"d"]);
assert_eq!(f.run(&[b"ZINTERCARD", b"2", b"z", b"y"]), ":2\r\n");
assert_eq!(
f.run(&[b"ZINTERCARD", b"2", b"z", b"y", b"LIMIT", b"0"]),
":2\r\n"
);
assert_eq!(
f.run(&[b"ZINTERCARD", b"2", b"z", b"y", b"LIMIT", b"1"]),
":1\r\n"
);
let bad = "-ERR LIMIT can't be negative\r\n";
assert_eq!(
f.run(&[b"ZINTERCARD", b"2", b"z", b"y", b"LIMIT", b"-1"]),
bad
);
assert_eq!(
f.run(&[b"ZINTERCARD", b"2", b"z", b"y", b"LIMIT", b"x"]),
bad
);
for cmd in [
&[b"ZINTERCARD".as_slice(), b"3", b"z", b"y"][..],
&[b"ZINTERCARD", b"2", b"z", b"y", b"LIMIT"],
&[b"ZINTERCARD", b"2", b"z", b"y", b"junk", b"1"],
] {
assert_eq!(f.run(cmd), "-ERR syntax error\r\n", "{cmd:?}");
}
}
#[test]
fn a_draw_answers_one_member_or_an_array_of_them() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(f.run(&[b"ZRANDMEMBER", b"nokey"]), "$-1\r\n");
assert_eq!(f.run(&[b"ZRANDMEMBER", b"nokey", b"3"]), "*0\r\n");
assert_eq!(f.run(&[b"ZRANDMEMBER", b"z", b"0"]), "*0\r\n");
assert!(f.run(&[b"ZRANDMEMBER", b"z"]).starts_with("$1\r\n"));
let all = f.run(&[b"ZRANDMEMBER", b"z", b"10"]);
assert!(all.starts_with("*3\r\n"), "{all}");
for m in ["a", "b", "c"] {
assert!(all.contains(m), "{all}");
}
assert!(
f.run(&[b"ZRANDMEMBER", b"z", b"-5"]).starts_with("*5\r\n"),
"five draws with replacement"
);
assert!(
f.run(&[b"ZRANDMEMBER", b"z", b"2", b"WITHSCORES"])
.starts_with("*4\r\n"),
"two pairs, flat on RESP2"
);
f.out = Out::new(Proto::Resp3);
let got = f.run(&[b"ZRANDMEMBER", b"z", b"2", b"WITHSCORES"]);
assert!(got.starts_with("*2\r\n*2\r\n"), "{got}");
assert_eq!(f.run(&[b"ZRANDMEMBER", b"nokey"]), "_\r\n");
f.out = Out::new(Proto::Resp2);
assert_eq!(
f.run(&[b"ZRANDMEMBER", b"z", b"2", b"junk"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"ZRANDMEMBER", b"z", b"x"]),
"-ERR value is not an integer or out of range\r\n"
);
}
#[test]
fn a_sorted_set_scan_answers_pairs_of_strings_on_both_protocols() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
let all = "*2\r\n$1\r\n0\r\n*6\r\n$1\r\na\r\n$1\r\n1\r\n$1\r\nb\r\n$1\r\n2\r\n$1\r\nc\r\n$1\r\n3\r\n";
assert_eq!(f.run(&[b"ZSCAN", b"z", b"0"]), all);
assert_eq!(f.run(&[b"ZSCAN", b"z", b"0", b"COUNT", b"10"]), all);
assert_eq!(
f.run(&[b"ZSCAN", b"z", b"0", b"MATCH", b"a*"]),
"*2\r\n$1\r\n0\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
assert_eq!(
f.run(&[b"ZSCAN", b"nokey", b"0"]),
"*2\r\n$1\r\n0\r\n*0\r\n"
);
f.out = Out::new(Proto::Resp3);
assert_eq!(f.run(&[b"ZSCAN", b"z", b"0"]), all);
f.out = Out::new(Proto::Resp2);
assert_eq!(
f.run(&[b"ZSCAN", b"z", b"0", b"NOVALUES"]),
"-ERR NOVALUES option can only be used in HSCAN\r\n"
);
assert_eq!(f.run(&[b"ZSCAN", b"z", b"-1"]), "-ERR invalid cursor\r\n");
assert_eq!(
f.run(&[b"ZSCAN", b"z", b"0", b"COUNT", b"0"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_sorted_set_pop_changes_shape_when_it_is_given_a_count() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(f.run(&[b"ZPOPMIN", b"z"]), "*2\r\n$1\r\na\r\n$1\r\n1\r\n");
assert_eq!(f.run(&[b"ZPOPMAX", b"z"]), "*2\r\n$1\r\nc\r\n$1\r\n3\r\n");
f.run(&[b"ZADD", b"z", b"1", b"a", b"3", b"c"]);
assert_eq!(
f.run(&[b"ZPOPMIN", b"z", b"2"]),
"*4\r\n$1\r\na\r\n$1\r\n1\r\n$1\r\nb\r\n$1\r\n2\r\n"
);
assert_eq!(f.run(&[b"ZPOPMIN", b"nokey"]), "*0\r\n");
assert_eq!(f.run(&[b"ZPOPMIN", b"nokey", b"2"]), "*0\r\n");
assert_eq!(f.run(&[b"ZPOPMIN", b"z", b"0"]), "*0\r\n");
assert_eq!(
f.run(&[b"ZPOPMIN", b"z", b"9"]),
"*2\r\n$1\r\nc\r\n$1\r\n3\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"z"]), ":0\r\n");
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b"]);
f.out = Out::new(Proto::Resp3);
assert_eq!(f.run(&[b"ZPOPMIN", b"z"]), "*2\r\n$1\r\na\r\n,1\r\n");
assert_eq!(
f.run(&[b"ZPOPMIN", b"z", b"1"]),
"*1\r\n*2\r\n$1\r\nb\r\n,2\r\n"
);
f.out = Out::new(Proto::Resp2);
let bad = "-ERR value is out of range, must be positive\r\n";
assert_eq!(f.run(&[b"ZPOPMIN", b"z", b"x"]), bad);
assert_eq!(f.run(&[b"ZPOPMIN", b"z", b"-1"]), bad);
assert_eq!(
f.run(&[b"ZPOPMIN", b"z", b"1", b"2"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_multi_key_pop_names_the_key_that_answered_and_nests_its_pairs() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(
f.run(&[b"ZMPOP", b"2", b"nokey", b"z", b"MIN"]),
"*2\r\n$1\r\nz\r\n*1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
assert_eq!(
f.run(&[b"ZMPOP", b"1", b"z", b"MAX", b"COUNT", b"2"]),
"*2\r\n$1\r\nz\r\n*2\r\n*2\r\n$1\r\nc\r\n$1\r\n3\r\n*2\r\n$1\r\nb\r\n$1\r\n2\r\n"
);
assert_eq!(f.run(&[b"ZMPOP", b"1", b"nokey", b"MIN"]), "*-1\r\n");
f.out = Out::new(Proto::Resp3);
assert_eq!(f.run(&[b"ZMPOP", b"1", b"nokey", b"MIN"]), "_\r\n");
f.out = Out::new(Proto::Resp2);
let numkeys = "-ERR numkeys should be greater than 0\r\n";
for bad in [
&[b"ZMPOP".as_slice(), b"0", b"z", b"MIN"][..],
&[b"ZMPOP", b"-1", b"z", b"MIN"],
&[b"ZMPOP", b"x", b"z", b"MIN"],
] {
assert_eq!(f.run(bad), numkeys, "{:?}", bad[1]);
}
let count = "-ERR count should be greater than 0\r\n";
for bad in [
&[b"ZMPOP".as_slice(), b"1", b"z", b"MIN", b"COUNT", b"0"][..],
&[b"ZMPOP", b"1", b"z", b"MIN", b"COUNT", b"-1"],
&[b"ZMPOP", b"1", b"z", b"MIN", b"COUNT", b"x"],
] {
assert_eq!(f.run(bad), count, "{:?}", bad[5]);
}
let syntax = "-ERR syntax error\r\n";
for bad in [
&[b"ZMPOP".as_slice(), b"2", b"z", b"MIN"][..],
&[b"ZMPOP", b"1", b"z", b"SIDEWAYS"],
&[b"ZMPOP", b"1", b"z", b"MIN", b"junk"],
&[b"ZMPOP", b"1", b"z", b"MIN", b"COUNT", b"1", b"junk"],
] {
assert_eq!(f.run(bad), syntax, "{bad:?}");
}
}
#[test]
fn the_sorted_set_pops_that_wait_answer_like_the_ones_they_wrap() {
let mut f = Fixture::new();
f.run(&[b"ZADD", b"z", b"1", b"a", b"2", b"b", b"3", b"c"]);
assert_eq!(
f.flow(&[b"BZPOPMIN", b"nokey", b"z", b"0"]),
(
Flow::Continue,
"*3\r\n$1\r\nz\r\n$1\r\na\r\n$1\r\n1\r\n".to_owned()
)
);
assert_eq!(
f.run(&[b"BZPOPMAX", b"z", b"0"]),
"*3\r\n$1\r\nz\r\n$1\r\nc\r\n$1\r\n3\r\n"
);
f.run(&[b"ZADD", b"z", b"1", b"a", b"3", b"c"]);
assert_eq!(
f.run(&[
b"BZMPOP", b"0", b"2", b"nokey", b"z", b"MIN", b"COUNT", b"2"
]),
"*2\r\n$1\r\nz\r\n*2\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n*2\r\n$1\r\nb\r\n$1\r\n2\r\n"
);
f.out = Out::new(Proto::Resp3);
assert_eq!(
f.run(&[b"BZPOPMIN", b"z", b"0"]),
"*3\r\n$1\r\nz\r\n$1\r\nc\r\n,3\r\n"
);
f.out = Out::new(Proto::Resp2);
assert_eq!(
f.flow(&[b"BZPOPMIN", b"z", b"0"]),
(Flow::Block, String::new())
);
assert_eq!(
f.flow(&[b"BZMPOP", b"0", b"1", b"z", b"MIN"]),
(Flow::Block, String::new())
);
assert_eq!(
f.run(&[b"BZMPOP", b"abc", b"0", b"z", b"MIN"]),
"-ERR timeout is not a float or out of range\r\n"
);
assert_eq!(
f.run(&[b"BZMPOP", b"0", b"0", b"z", b"MIN"]),
"-ERR numkeys should be greater than 0\r\n"
);
assert_eq!(
f.run(&[b"BZPOPMIN", b"z", b"-1"]),
"-ERR timeout is negative\r\n"
);
}
#[test]
fn a_parked_sorted_set_client_waits_for_a_member_and_not_for_a_key() {
let mut f = Fixture::new();
assert_eq!(f.flow(&[b"BZPOPMIN", b"z", b"0"]).0, Flow::Block);
assert_eq!(f.server.waiters().len(), 1);
f.run(&[b"SET", b"z", b"v"]);
let mut out = Out::new(Proto::Resp2);
assert!(!f.server.serve_waiter(0, 0, &mut out));
assert!(out.as_slice().is_empty());
f.run(&[b"DEL", b"z"]);
f.run(&[b"ZADD", b"z", b"5", b"m"]);
assert!(f.server.serve_waiter(0, 0, &mut out));
assert_eq!(
core::str::from_utf8(out.as_slice()).expect("ascii"),
"*3\r\n$1\r\nz\r\n$1\r\nm\r\n$1\r\n5\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"z"]), ":0\r\n");
}
#[test]
fn every_sorted_set_command_says_wrongtype_and_writes_nothing() {
let mut f = Fixture::new();
f.run(&[b"SET", b"s", b"v"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
for cmd in [
&[b"ZADD".as_slice(), b"s", b"1", b"a"][..],
&[b"ZINCRBY", b"s", b"1", b"a"],
&[b"ZCARD", b"s"],
&[b"ZSCORE", b"s", b"a"],
&[b"ZMSCORE", b"s", b"a"],
&[b"ZREM", b"s", b"a"],
&[b"ZRANK", b"s", b"a"],
&[b"ZREVRANK", b"s", b"a"],
&[b"ZCOUNT", b"s", b"1", b"2"],
&[b"ZLEXCOUNT", b"s", b"-", b"+"],
&[b"ZRANGE", b"s", b"0", b"-1"],
&[b"ZREVRANGE", b"s", b"0", b"-1"],
&[b"ZRANGEBYSCORE", b"s", b"1", b"2"],
&[b"ZREVRANGEBYSCORE", b"s", b"2", b"1"],
&[b"ZRANGEBYLEX", b"s", b"-", b"+"],
&[b"ZREVRANGEBYLEX", b"s", b"+", b"-"],
&[b"ZRANGESTORE", b"d", b"s", b"0", b"-1"],
&[b"ZREMRANGEBYRANK", b"s", b"0", b"-1"],
&[b"ZREMRANGEBYSCORE", b"s", b"1", b"2"],
&[b"ZREMRANGEBYLEX", b"s", b"-", b"+"],
&[b"ZUNION", b"1", b"s"],
&[b"ZINTER", b"1", b"s"],
&[b"ZDIFF", b"1", b"s"],
&[b"ZUNIONSTORE", b"d", b"1", b"s"],
&[b"ZINTERSTORE", b"d", b"1", b"s"],
&[b"ZDIFFSTORE", b"d", b"1", b"s"],
&[b"ZINTERCARD", b"1", b"s"],
&[b"ZRANDMEMBER", b"s"],
&[b"ZSCAN", b"s", b"0"],
&[b"ZPOPMIN", b"s"],
&[b"ZPOPMAX", b"s", b"2"],
&[b"ZMPOP", b"1", b"s", b"MIN"],
&[b"BZPOPMIN", b"s", b"0"],
&[b"BZPOPMAX", b"s", b"0"],
&[b"BZMPOP", b"0", b"1", b"s", b"MIN"],
] {
assert_eq!(f.run(cmd), wrong, "{:?}", cmd[0]);
}
assert_eq!(f.run(&[b"GET", b"s"]), "$1\r\nv\r\n");
}
#[test]
fn churning_sorted_sets_does_not_grow_the_server() {
let mut f = Fixture::new();
let members: Vec<Vec<u8>> = (0..200).map(|i| format!("m{i}").into_bytes()).collect();
let scores: Vec<Vec<u8>> = (0..200).map(|i| format!("{i}").into_bytes()).collect();
let mut args: Vec<&[u8]> = vec![b"ZADD", b"z"];
for i in 0..200 {
args.push(&scores[i]);
args.push(&members[i]);
}
f.run(&args);
f.run(&[b"DEL", b"z"]);
f.server.compact_step();
let after_first = f.server.memory_bytes();
for _ in 0..200 {
f.run(&args);
f.run(&[b"DEL", b"z"]);
f.server.compact_step();
}
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
assert!(
f.server.memory_bytes() <= after_first * 2,
"held {} after two hundred passes against {after_first} after one",
f.server.memory_bytes()
);
}
#[test]
fn an_array_writes_at_any_index_and_reads_back_what_it_sent() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"ARSET", b"a", b"1000", b"x", b"y", b"z"]),
":3\r\n"
);
assert_eq!(f.run(&[b"ARSET", b"a", b"1000", b"X", b"Y"]), ":0\r\n");
assert_eq!(f.run(&[b"ARGET", b"a", b"1000"]), "$1\r\nX\r\n");
assert_eq!(f.run(&[b"ARGET", b"a", b"1002"]), "$1\r\nz\r\n");
assert_eq!(f.run(&[b"ARGET", b"a", b"999"]), "$-1\r\n");
assert_eq!(f.run(&[b"ARGET", b"nope", b"0"]), "$-1\r\n");
assert_eq!(
f.run(&[b"ARMGET", b"a", b"1002", b"999", b"1000"]),
"*3\r\n$1\r\nz\r\n$-1\r\n$1\r\nX\r\n"
);
assert_eq!(f.run(&[b"ARMSET", b"a", b"5", b"p", b"5", b"q"]), ":1\r\n");
assert_eq!(f.run(&[b"ARGET", b"a", b"5"]), "$1\r\nq\r\n");
}
#[test]
fn the_length_is_the_high_water_mark_and_the_count_is_the_population() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"ARLEN", b"nope"]), ":0\r\n");
assert_eq!(f.run(&[b"ARCOUNT", b"nope"]), ":0\r\n");
f.run(&[b"ARMSET", b"a", b"0", b"x", b"9", b"y"]);
assert_eq!(f.run(&[b"ARLEN", b"a"]), ":10\r\n");
assert_eq!(f.run(&[b"ARCOUNT", b"a"]), ":2\r\n");
assert_eq!(f.run(&[b"ARDEL", b"a", b"0"]), ":1\r\n");
assert_eq!(f.run(&[b"ARLEN", b"a"]), ":10\r\n");
assert_eq!(f.run(&[b"ARCOUNT", b"a"]), ":1\r\n");
f.run(&[b"ARSET", b"top", b"18446744073709551614", b"z"]);
assert_eq!(f.run(&[b"ARLEN", b"top"]), ":18446744073709551615\r\n");
assert_eq!(f.run(&[b"ARCOUNT", b"top"]), ":1\r\n");
assert_eq!(
f.run(&[b"ARSET", b"over", b"18446744073709551614", b"a", b"b"]),
"-ERR array index overflow\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"over"]), ":0\r\n");
}
#[test]
fn a_range_read_answers_for_the_holes_too_and_is_capped_at_a_million() {
let mut f = Fixture::new();
f.run(&[b"ARSET", b"a", b"1", b"x"]);
assert_eq!(
f.run(&[b"ARGETRANGE", b"a", b"0", b"3"]),
"*4\r\n$-1\r\n$1\r\nx\r\n$-1\r\n$-1\r\n"
);
assert_eq!(
f.run(&[b"ARGETRANGE", b"a", b"3", b"0"]),
"*4\r\n$-1\r\n$-1\r\n$1\r\nx\r\n$-1\r\n"
);
assert_eq!(
f.run(&[b"ARGETRANGE", b"nope", b"0", b"1"]),
"*2\r\n$-1\r\n$-1\r\n"
);
assert_eq!(
f.run(&[b"ARGETRANGE", b"nope", b"0", b"18446744073709551614"]),
"-ERR range exceeds maximum of 1000000 items\r\n"
);
}
#[test]
fn a_bad_index_late_in_the_line_writes_none_of_the_earlier_ones() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"ARMSET", b"a", b"0", b"x", b"-1", b"y"]),
"-ERR invalid array index\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"a"]), ":0\r\n");
f.run(&[b"ARSET", b"a", b"0", b"x", b"y", b"z"]);
assert_eq!(
f.run(&[b"ARDEL", b"a", b"0", b"01"]),
"-ERR invalid array index\r\n"
);
assert_eq!(f.run(&[b"ARCOUNT", b"a"]), ":3\r\n");
assert_eq!(
f.run(&[b"ARGET", b"a", b"-1"]),
"-ERR invalid array index\r\n"
);
assert_eq!(
f.run(&[b"ARMSET", b"a", b"0", b"x", b"1"]),
"-ERR wrong number of arguments for 'armset' command\r\n"
);
assert_eq!(
f.run(&[b"ARDELRANGE", b"a", b"0", b"1", b"2"]),
"-ERR wrong number of arguments for 'ardelrange' command\r\n"
);
}
#[test]
fn a_range_delete_costs_the_elements_and_takes_the_key_when_it_empties() {
let mut f = Fixture::new();
f.run(&[b"ARSET", b"a", b"0", b"0", b"1", b"2", b"3", b"4"]);
assert_eq!(f.run(&[b"ARDELRANGE", b"a", b"3", b"1"]), ":3\r\n");
assert_eq!(f.run(&[b"ARCOUNT", b"a"]), ":2\r\n");
assert_eq!(
f.run(&[
b"ARDELRANGE",
b"a",
b"100",
b"200",
b"0",
b"18446744073709551614"
]),
":2\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"ARDELRANGE", b"nope", b"0", b"1"]), ":0\r\n");
assert_eq!(f.run(&[b"ARDEL", b"nope", b"0"]), ":0\r\n");
}
#[test]
fn a_value_comes_back_byte_for_byte_however_it_was_packed() {
let mut f = Fixture::new();
let long = vec![b'v'; 200];
f.run(&[
b"ARMSET", b"a", b"0", b"42", b"1", b"007", b"2", b"3.5", b"3", b"3.14", b"4",
b"short", b"5", &long, b"6", b"-0",
]);
assert_eq!(
f.run(&[b"ARGETRANGE", b"a", b"0", b"6"]),
format!(
"*7\r\n$2\r\n42\r\n$3\r\n007\r\n$3\r\n3.5\r\n$4\r\n3.14\r\n$5\r\nshort\r\n$200\r\n{}\r\n$2\r\n-0\r\n",
String::from_utf8_lossy(&long)
)
);
}
#[test]
fn an_array_is_a_type_and_an_encoding_a_client_can_see() {
let mut f = Fixture::new();
f.run(&[b"ARSET", b"a", b"0", b"x"]);
assert_eq!(f.run(&[b"TYPE", b"a"]), "+array\r\n");
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING", b"a"]),
"$12\r\nsliced-array\r\n"
);
assert_eq!(f.run(&[b"EXPIRE", b"a", b"100"]), ":1\r\n");
assert_eq!(f.run(&[b"PERSIST", b"a"]), ":1\r\n");
assert_eq!(f.run(&[b"COPY", b"a", b"b"]), ":1\r\n");
assert_eq!(f.run(&[b"ARGET", b"b", b"0"]), "$1\r\nx\r\n");
assert_eq!(f.run(&[b"RENAME", b"a", b"c"]), "+OK\r\n");
assert_eq!(f.run(&[b"ARCOUNT", b"c"]), ":1\r\n");
}
#[test]
fn every_array_command_refuses_a_key_holding_something_else() {
let mut f = Fixture::new();
f.run(&[b"SET", b"s", b"v"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
for cmd in [
&[b"ARSET".as_ref(), b"s", b"0", b"x"][..],
&[b"ARMSET".as_ref(), b"s", b"0", b"x"][..],
&[b"ARGET".as_ref(), b"s", b"0"][..],
&[b"ARMGET".as_ref(), b"s", b"0"][..],
&[b"ARGETRANGE".as_ref(), b"s", b"0", b"1"][..],
&[b"ARLEN".as_ref(), b"s"][..],
&[b"ARCOUNT".as_ref(), b"s"][..],
&[b"ARDEL".as_ref(), b"s", b"0"][..],
&[b"ARDELRANGE".as_ref(), b"s", b"0", b"1"][..],
&[b"ARINSERT".as_ref(), b"s", b"x"][..],
&[b"ARRING".as_ref(), b"s", b"4", b"x"][..],
&[b"ARNEXT".as_ref(), b"s"][..],
&[b"ARSEEK".as_ref(), b"s", b"1"][..],
&[b"ARLASTITEMS".as_ref(), b"s", b"1"][..],
&[b"ARSCAN".as_ref(), b"s", b"0", b"1"][..],
&[b"AROP".as_ref(), b"s", b"0", b"1", b"SUM"][..],
&[b"ARINFO".as_ref(), b"s"][..],
] {
assert_eq!(f.run(cmd), wrong, "{}", String::from_utf8_lossy(cmd[0]));
}
}
#[test]
fn a_bad_index_reports_the_type_only_where_redis_reports_it() {
let mut f = Fixture::new();
f.run(&[b"SET", b"s", b"v"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
let bad = "-ERR invalid array index\r\n";
assert_eq!(f.run(&[b"ARGET", b"s", b"-1"]), wrong);
assert_eq!(f.run(&[b"ARMGET", b"s", b"0", b"-1"]), wrong);
assert_eq!(f.run(&[b"ARSET", b"s", b"-1", b"x"]), bad);
assert_eq!(f.run(&[b"ARDEL", b"s", b"-1"]), bad);
assert_eq!(f.run(&[b"ARSCAN", b"s", b"-1", b"0"]), bad);
f.run(&[b"ARSET", b"a", b"0", b"x"]);
assert_eq!(f.run(&[b"ARGET", b"a", b"-1"]), bad);
assert_eq!(f.run(&[b"ARGET", b"nope", b"-1"]), bad);
}
#[test]
fn an_append_follows_a_cursor_the_client_can_move() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"ARNEXT", b"nope"]), ":0\r\n");
assert_eq!(f.run(&[b"ARINSERT", b"a", b"x", b"y"]), ":1\r\n");
assert_eq!(f.run(&[b"ARNEXT", b"a"]), ":2\r\n");
assert_eq!(f.run(&[b"ARINSERT", b"a", b"z"]), ":2\r\n");
assert_eq!(f.run(&[b"ARGET", b"a", b"2"]), "$1\r\nz\r\n");
assert_eq!(f.run(&[b"ARSEEK", b"nope", b"5"]), ":0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"nope"]), ":0\r\n");
assert_eq!(f.run(&[b"ARSEEK", b"a", b"100"]), ":1\r\n");
assert_eq!(f.run(&[b"ARNEXT", b"a"]), ":100\r\n");
assert_eq!(f.run(&[b"ARINSERT", b"a", b"far"]), ":100\r\n");
assert_eq!(f.run(&[b"ARSEEK", b"a", b"0"]), ":1\r\n");
assert_eq!(f.run(&[b"ARNEXT", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"ARSEEK", b"a", b"18446744073709551615"]), ":1\r\n");
assert_eq!(f.run(&[b"ARNEXT", b"a"]), "$-1\r\n");
assert_eq!(
f.run(&[b"ARINSERT", b"a", b"x"]),
"-ERR insert index overflow\r\n"
);
assert_eq!(
f.run(&[b"ARSET", b"a", b"18446744073709551615", b"x"]),
"-ERR invalid array index\r\n"
);
}
#[test]
fn a_ring_keeps_the_newest_and_renumbers_them_when_it_is_resized() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"ARRING", b"r", b"3", b"a", b"b", b"c"]), ":2\r\n");
assert_eq!(f.run(&[b"ARRING", b"r", b"3", b"d", b"e"]), ":1\r\n");
assert_eq!(f.run(&[b"ARLEN", b"r"]), ":3\r\n");
assert_eq!(
f.run(&[b"ARGETRANGE", b"r", b"0", b"2"]),
"*3\r\n$1\r\nd\r\n$1\r\ne\r\n$1\r\nc\r\n"
);
assert_eq!(f.run(&[b"ARRING", b"r", b"5", b"f"]), ":3\r\n");
assert_eq!(
f.run(&[b"ARGETRANGE", b"r", b"0", b"3"]),
"*4\r\n$1\r\nc\r\n$1\r\nd\r\n$1\r\ne\r\n$1\r\nf\r\n"
);
assert_eq!(
f.run(&[b"ARRING", b"r", b"0", b"x"]),
"-ERR size must be positive\r\n"
);
assert_eq!(
f.run(&[b"ARRING", b"r", b"big", b"x"]),
"-ERR invalid size\r\n"
);
}
#[test]
fn the_last_items_walk_back_from_the_cursor_and_report_the_holes() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"ARLASTITEMS", b"nope", b"5"]), "*0\r\n");
f.run(&[b"ARRING", b"r", b"4", b"a", b"b", b"c", b"d", b"e"]);
assert_eq!(
f.run(&[b"ARLASTITEMS", b"r", b"3"]),
"*3\r\n$1\r\nc\r\n$1\r\nd\r\n$1\r\ne\r\n"
);
assert_eq!(
f.run(&[b"ARLASTITEMS", b"r", b"3", b"rev"]),
"*3\r\n$1\r\ne\r\n$1\r\nd\r\n$1\r\nc\r\n"
);
assert_eq!(
f.run(&[b"ARLASTITEMS", b"r", b"99"]),
"*4\r\n$1\r\nb\r\n$1\r\nc\r\n$1\r\nd\r\n$1\r\ne\r\n",
"more than there is gets what there is"
);
assert_eq!(f.run(&[b"ARLASTITEMS", b"r", b"0", b"junk"]), "*0\r\n");
assert_eq!(
f.run(&[b"ARLASTITEMS", b"r", b"1", b"junk"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"ARLASTITEMS", b"r", b"nine"]),
"-ERR invalid COUNT\r\n"
);
f.run(&[b"ARMSET", b"h", b"0", b"x", b"2", b"z"]);
assert_eq!(
f.run(&[b"ARLASTITEMS", b"h", b"5"]),
"*2\r\n$-1\r\n$1\r\nz\r\n"
);
}
#[test]
fn a_scan_answers_pairs_for_what_is_there_and_skips_what_is_not() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"ARSCAN", b"nope", b"0", b"10"]), "*0\r\n");
f.run(&[b"ARMSET", b"a", b"0", b"x", b"7", b"y", b"1000000", b"z"]);
assert_eq!(
f.run(&[b"ARSCAN", b"a", b"0", b"18446744073709551614"]),
"*3\r\n*2\r\n:0\r\n$1\r\nx\r\n*2\r\n:7\r\n$1\r\ny\r\n*2\r\n:1000000\r\n$1\r\nz\r\n"
);
assert_eq!(
f.run(&[
b"ARSCAN",
b"a",
b"18446744073709551614",
b"0",
b"LIMIT",
b"1"
]),
"*1\r\n*2\r\n:1000000\r\n$1\r\nz\r\n"
);
assert_eq!(f.run(&[b"ARSCAN", b"a", b"1", b"6"]), "*0\r\n");
assert_eq!(
f.run(&[b"ARSCAN", b"a", b"0", b"10", b"LIMIT", b"0"]),
"-ERR LIMIT must be positive\r\n"
);
assert_eq!(
f.run(&[b"ARSCAN", b"a", b"0", b"10", b"NOPE", b"1"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"ARSCAN", b"a", b"0", b"10", b"LIMIT"]),
"-ERR wrong number of arguments for 'arscan' command\r\n"
);
}
#[test]
fn an_op_reduces_a_range_to_one_number() {
let mut f = Fixture::new();
f.run(&[b"ARSET", b"a", b"0", b"1", b"2.5", b"word", b"-4"]);
assert_eq!(
f.run(&[b"AROP", b"a", b"0", b"10", b"SUM"]),
"$4\r\n-0.5\r\n"
);
assert_eq!(f.run(&[b"AROP", b"a", b"0", b"10", b"min"]), "$2\r\n-4\r\n");
assert_eq!(
f.run(&[b"AROP", b"a", b"0", b"10", b"MAX"]),
"$3\r\n2.5\r\n"
);
assert_eq!(f.run(&[b"AROP", b"a", b"0", b"10", b"USED"]), ":4\r\n");
assert_eq!(
f.run(&[b"AROP", b"a", b"0", b"10", b"MATCH", b"word"]),
":1\r\n"
);
f.run(&[b"ARSET", b"t", b"0", b"0.1", b"0.2"]);
assert_eq!(
f.run(&[b"AROP", b"t", b"0", b"10", b"SUM"]),
"$19\r\n0.30000000000000004\r\n"
);
assert_eq!(f.run(&[b"ZADD", b"z", b"0.3", b"m"]), ":1\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z", b"m"]), "$3\r\n0.3\r\n");
f.run(&[b"ARSET", b"w", b"0", b"word"]);
assert_eq!(f.run(&[b"AROP", b"w", b"0", b"10", b"SUM"]), "$-1\r\n");
assert_eq!(f.run(&[b"AROP", b"nope", b"0", b"10", b"SUM"]), "$-1\r\n");
assert_eq!(f.run(&[b"AROP", b"nope", b"0", b"10", b"USED"]), ":0\r\n");
assert_eq!(
f.run(&[b"AROP", b"a", b"0", b"10", b"NOPE"]),
"-ERR unknown operation\r\n"
);
assert_eq!(
f.run(&[b"AROP", b"a", b"0", b"10", b"MATCH"]),
"-ERR MATCH requires a value argument\r\n"
);
assert_eq!(
f.run(&[b"AROP", b"a", b"0", b"10", b"SUM", b"extra"]),
"-ERR wrong number of arguments for 'arop' command\r\n"
);
}
#[test]
fn the_info_is_a_map_and_a_missing_key_is_an_error() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"ARINFO", b"nope"]), "-ERR no such key\r\n");
f.run(&[b"ARINSERT", b"a", b"x", b"y"]);
let short = f.run(&[b"ARINFO", b"a"]);
assert!(
short.starts_with("*14\r\n"),
"seven pairs on RESP2: {short}"
);
assert!(short.contains("$5\r\ncount\r\n:2\r\n"), "{short}");
assert!(
short.contains("$17\r\nnext-insert-index\r\n:2\r\n"),
"{short}"
);
assert!(short.contains("$10\r\nslice-size\r\n:4096\r\n"), "{short}");
let full = f.run(&[b"ARINFO", b"a", b"full"]);
assert!(full.starts_with("*24\r\n"), "twelve pairs: {full}");
assert!(full.contains("$12\r\ndense-slices\r\n:0\r\n"), "{full}");
assert!(full.contains("$13\r\nsparse-slices\r\n:1\r\n"), "{full}");
assert!(
full.contains("$14\r\navg-dense-size\r\n$1\r\n0\r\n"),
"{full}"
);
assert_eq!(f.run(&[b"ARINFO", b"a", b"nope"]), "-ERR syntax error\r\n");
let mut g = Fixture::new();
g.run(&[b"HELLO", b"3"]);
g.run(&[b"ARINSERT", b"a", b"x"]);
let map = g.run(&[b"ARINFO", b"a", b"FULL"]);
assert!(map.starts_with("%12\r\n"), "{map}");
assert!(map.contains("$5\r\ncount\r\n:1\r\n"), "{map}");
assert!(map.contains("$14\r\navg-dense-size\r\n,0\r\n"), "{map}");
}
#[test]
fn a_double_on_the_wire_is_written_the_way_redis_writes_one() {
let mut f = Fixture::new();
for (score, want) in [
("3", "3"),
("3.5", "3.5"),
("0.3", "0.3"),
("1e30", "1e+30"),
("1e19", "1e+19"),
("1e-7", "1e-7"),
("0.000001", "0.000001"),
("4611686018427387904", "4611686018427387904"),
("-0", "-0"),
] {
f.run(&[b"ZADD", b"z", score.as_bytes(), b"m"]);
assert_eq!(
f.run(&[b"ZSCORE", b"z", b"m"]),
format!("${}\r\n{want}\r\n", want.len()),
"score {score}"
);
}
let mut g = Fixture::new();
g.run(&[b"HELLO", b"3"]);
g.run(&[b"ZADD", b"z", b"1e30", b"m"]);
assert_eq!(g.run(&[b"ZSCORE", b"z", b"m"]), ",1e+30\r\n");
assert_eq!(
g.run(&[b"INCRBYFLOAT", b"s", b"1e30"]),
"$31\r\n1000000000000000000000000000000\r\n"
);
assert_eq!(g.run(&[b"INCRBYFLOAT", b"t", b"0.1"]), "$3\r\n0.1\r\n");
assert_eq!(
g.run(&[b"HINCRBYFLOAT", b"h", b"f", b"1e19"]),
"$20\r\n10000000000000000000\r\n"
);
}
}