mod args;
mod arrays;
mod bits;
mod blocking;
mod cpu;
mod geo;
mod graph;
mod hashes;
mod hll;
mod keyspace;
mod lists;
mod migrate;
mod scan;
mod scripting;
mod server;
mod sets;
mod streams;
mod strings;
pub mod table;
mod zsets;
pub use args::Args;
pub use blocking::{Parked, Waiters};
pub use server::parse_memory;
pub use table::{COMMANDS, Spec, arity_ok, lookup};
use crate::reply::Out;
use yo_common::{Code, Error};
use yo_kv::cold::Blocks;
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);
const EVICT_BUDGET: usize = 64;
const OOM: &[u8] = b"command not allowed when used memory > 'maxmemory'.";
#[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,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CommandStat {
pub calls: u64,
pub rejected: u64,
pub failed: u64,
}
impl CommandStat {
const fn seen(&self) -> bool {
self.calls != 0 || self.rejected != 0 || self.failed != 0
}
}
struct CommandStats(Box<[CommandStat]>);
impl Default for CommandStats {
fn default() -> CommandStats {
CommandStats(vec![CommandStat::default(); table::count()].into_boxed_slice())
}
}
impl CommandStats {
fn at(&mut self, spec: &'static Spec) -> &mut CommandStat {
&mut self.0[table::index_of(spec)]
}
}
pub type StoreSource = dyn FnMut(usize) -> Option<Box<dyn Blocks>>;
pub struct Server {
dbs: Vec<Keyspace>,
clock: Clock,
started_ms: u64,
next_db: usize,
dirty: u64,
conn_bytes: usize,
maxmemory: u64,
store: Option<Box<StoreSource>>,
maxstore: Option<u64>,
used: usize,
evict_db: usize,
expire_db: usize,
expire_ms: u64,
waiters: Waiters,
peers: migrate::Peers,
pub stats: Stats,
cmdstats: CommandStats,
}
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,
maxmemory: 0,
store: None,
maxstore: None,
used: 0,
evict_db: 0,
expire_db: 0,
expire_ms: 0,
waiters: Waiters::default(),
peers: migrate::Peers::default(),
stats: Stats::default(),
cmdstats: CommandStats::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,
maxmemory: 0,
store: None,
maxstore: None,
used: 0,
evict_db: 0,
expire_db: 0,
expire_ms: 0,
waiters: Waiters::default(),
peers: migrate::Peers::default(),
stats: Stats::default(),
cmdstats: CommandStats::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()
}
#[must_use]
pub fn evicted_keys(&self) -> u64 {
self.dbs.iter().map(Keyspace::evicted_keys).sum()
}
pub fn command_stats(&self) -> impl Iterator<Item = (&'static str, CommandStat)> {
self.cmdstats
.0
.iter()
.enumerate()
.filter(|(_, row)| row.seen())
.map(|(at, row)| (table::name_at(at), *row))
}
#[must_use]
pub const fn maxmemory(&self) -> u64 {
self.maxmemory
}
pub fn set_maxmemory(&mut self, bytes: u64) {
self.maxmemory = bytes;
for db in &mut self.dbs {
db.track_memory(bytes != 0);
}
self.used = self.settled_memory();
}
pub fn set_store_source(
&mut self,
source: impl FnMut(usize) -> Option<Box<dyn Blocks>> + 'static,
) {
self.store = Some(Box::new(source));
}
#[must_use]
pub const fn has_store_source(&self) -> bool {
self.store.is_some()
}
fn attach_store(&mut self, at: usize) {
if self.dbs[at].store_bytes().is_some() {
return;
}
let Some(source) = self.store.as_mut() else {
return;
};
if let Some(blocks) = source(at) {
self.dbs[at].attach(blocks);
}
}
#[must_use]
pub const fn maxstore(&self) -> Option<u64> {
self.maxstore
}
pub const fn set_maxstore(&mut self, bytes: Option<u64>) {
self.maxstore = bytes;
}
#[must_use]
pub fn store_bytes(&self) -> u64 {
self.dbs.iter().filter_map(Keyspace::store_bytes).sum()
}
#[must_use]
pub fn cold_stats(&self) -> yo_kv::tier::Stats {
let mut total = yo_kv::tier::Stats::default();
for db in &self.dbs {
let Some(tier) = db.tier() else { continue };
let s = tier.stats();
total.demoted += s.demoted;
total.promoted += s.promoted;
total.faults += s.faults;
total.served += s.served;
total.bytes_out += s.bytes_out;
total.bytes_in += s.bytes_in;
}
total
}
#[must_use]
pub fn regime(&self) -> &'static str {
if (0..self.dbs.len()).any(|at| self.migrates(at)) {
"migrate"
} else {
"evict"
}
}
fn migrates(&self, at: usize) -> bool {
if self.maxstore == Some(0) {
return false;
}
match self.dbs[at].store_bytes() {
Some(held) => self.maxstore.is_none_or(|cap| held < cap),
None => self.store.is_some(),
}
}
pub fn refresh_memory(&mut self) {
if self.maxmemory != 0 {
self.used = self.settled_memory();
}
}
fn settled_memory(&mut self) -> usize {
self.dbs
.iter_mut()
.map(Keyspace::settled_memory_bytes)
.sum::<usize>()
+ self.conn_bytes
}
pub fn make_room(&mut self) -> bool {
if self.maxmemory == 0 || self.used as u64 <= self.maxmemory {
return true;
}
self.used = self.settled_memory();
let mut budget = EVICT_BUDGET;
while self.used as u64 > self.maxmemory {
let over = self.used - self.maxmemory as usize;
if !self.relieve_step(over) {
return false;
}
self.compact_hard_step();
self.used = self.settled_memory();
budget -= 1;
if budget == 0 {
break;
}
}
true
}
fn relieve_step(&mut self, over: usize) -> bool {
for turn in 0..self.dbs.len() {
let i = (self.evict_db + turn) % self.dbs.len();
let gave = if !self.dbs[i].is_empty() && self.migrates(i) {
self.attach_store(i);
self.dbs[i]
.relieve(over)
.is_ok_and(yo_kv::tier::Relief::made_room)
} else {
self.dbs[i].evict_one()
};
if gave {
self.evict_db = (i + 1) % self.dbs.len();
self.dirty |= 1u64 << i;
return true;
}
}
false
}
pub fn expire_slice(&mut self, budget: usize) -> usize {
let now = self.clock.now_ms();
if now == self.expire_ms {
return 0;
}
self.expire_ms = now;
self.expire_step(budget)
}
pub fn expire_step(&mut self, budget: usize) -> usize {
let mut spent = 0;
for turn in 0..self.dbs.len() {
if spent >= budget {
break;
}
let i = (self.expire_db + turn) % self.dbs.len();
let c = self.dbs[i].expire_cycle(budget - spent);
spent += c.examined;
if c.expired > 0 {
self.expire_db = (i + 1) % self.dbs.len();
self.dirty |= 1u64 << i;
}
}
spent
}
fn compact_hard_step(&mut self) -> Option<usize> {
for turn in 0..self.dbs.len() {
let i = (self.next_db + turn) % self.dbs.len();
if let Some(moved) = self.dbs[i].compact_hard() {
self.next_db = (i + 1) % self.dbs.len();
return Some(moved);
}
}
None
}
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;
}
resolved(server, session, lookup(args.name()), args, out)
}
pub fn resolved(
server: &mut Server,
session: &mut Session,
spec: Option<&'static Spec>,
args: Args<'_>,
out: &mut Out,
) -> Flow {
if args.is_empty() {
return Flow::Continue;
}
server.stats.commands += 1;
let Some(spec) = spec else {
write_error(out, &args::unknown_command(args));
return Flow::Continue;
};
if !arity_ok(spec, args.len()) {
server.cmdstats.at(spec).rejected += 1;
write_error(out, &args::wrong_arity(spec.name));
return Flow::Continue;
}
if server.maxmemory != 0 && !server.make_room() && spec.flags.contains(&"denyoom") {
server.cmdstats.at(spec).rejected += 1;
out.error_line(b"OOM ", OOM);
return Flow::Continue;
}
server.dirty |= match spec.group {
"string" | "bitmap" | "hyperloglog" | "geo" | "set" | "hash" | "list" | "zset"
| "array" | "stream" => 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)
}
"bitmap" => {
let db = session.db;
bits::execute(&mut server.dbs[db], spec, args, out).map(|()| Flow::Continue)
}
"hyperloglog" => {
let db = session.db;
hll::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)
}
"geo" => {
let db = session.db;
geo::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)
}
"graph" => {
let db = session.db;
graph::execute(&mut server.dbs[db], spec, args, out).map(|()| Flow::Continue)
}
"stream" => {
let db = session.db;
let now = server.now_ms();
streams::execute(&mut server.dbs[db], spec, args, now, out).map(|()| Flow::Continue)
}
"keyspace" if spec.name == "migrate" => {
migrate::execute(server, session.db, 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),
}
};
let flow = match done {
Ok(flow) => flow,
Err(e) => {
out.truncate(mark);
write_error(out, &e);
Flow::Continue
}
};
let row = server.cmdstats.at(spec);
row.calls += 1;
if matches!(out.as_slice().get(mark), Some(b'-' | b'!')) {
row.failed += 1;
}
flow
}
fn write_error(out: &mut Out, e: &Error) {
let prefix: &[u8] = match e.code() {
Code::WrongType => b"WRONGTYPE ",
Code::Corrupt => b"INVALIDOBJ ",
_ => 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 raw(&mut self, parts: &[&[u8]]) -> Vec<u8> {
let wire = encode(parts);
self.argv.decode(&wire, &Limits::default()).unwrap();
self.out.clear();
execute(
&mut self.server,
&mut self.session,
Args::new(&self.argv, &wire),
&mut self.out,
);
self.out.as_slice().to_vec()
}
fn advance(&mut self, ms: u64) {
for db in 0..DATABASES {
self.server.db(db).clock_mut().advance(ms);
}
}
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 sort_takes_its_options_in_any_order_and_the_last_one_wins() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"l", b"3", b"1", b"2"]);
assert_eq!(
f.run(&[b"SORT", b"l"]),
"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n"
);
assert_eq!(
f.run(&[b"SORT", b"l", b"DESC", b"asc"]),
"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n"
);
assert_eq!(
f.run(&[b"sort", b"l", b"LIMIT", b"1", b"1", b"DESC"]),
"*1\r\n$1\r\n2\r\n"
);
}
#[test]
fn sort_reads_a_key_per_element_for_by_and_for_get() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"l", b"a", b"b"]);
f.run(&[b"MSET", b"w_a", b"2", b"w_b", b"1", b"d_b", b"bee"]);
assert_eq!(
f.run(&[b"SORT", b"l", b"BY", b"w_*", b"GET", b"#", b"GET", b"d_*"]),
"*4\r\n$1\r\nb\r\n$3\r\nbee\r\n$1\r\na\r\n$-1\r\n"
);
}
#[test]
fn sort_store_writes_a_list_and_answers_its_length() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"l", b"3", b"1", b"2"]);
assert_eq!(f.run(&[b"SORT", b"l", b"STORE", b"out"]), ":3\r\n");
assert_eq!(f.run(&[b"TYPE", b"out"]), "+list\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"out", b"0", b"-1"]),
"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n"
);
assert_eq!(f.run(&[b"SORT", b"missing", b"STORE", b"out"]), ":0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"out"]), ":0\r\n");
}
#[test]
fn sort_ro_does_not_know_the_word_store() {
let mut f = Fixture::new();
f.run(&[b"RPUSH", b"l", b"2", b"1"]);
assert_eq!(f.run(&[b"SORT_RO", b"l"]), "*2\r\n$1\r\n1\r\n$1\r\n2\r\n");
assert_eq!(
f.run(&[b"SORT_RO", b"l", b"STORE", b"d"]),
"-ERR syntax error\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"d"]), ":0\r\n");
}
#[test]
fn sort_refuses_what_it_cannot_sort() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SORT", b"nosuchkey"]), "*0\r\n");
f.run(&[b"SET", b"s", b"x"]);
assert_eq!(
f.run(&[b"SORT", b"s"]),
"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"
);
f.run(&[b"RPUSH", b"words", b"one", b"two"]);
assert_eq!(
f.run(&[b"SORT", b"words"]),
"-ERR One or more scores can't be converted into double\r\n"
);
assert_eq!(
f.run(&[b"SORT", b"words", b"ALPHA"]),
"*2\r\n$3\r\none\r\n$3\r\ntwo\r\n"
);
assert_eq!(f.run(&[b"SORT", b"words", b"BY"]), "-ERR syntax error\r\n");
}
#[test]
fn move_takes_the_key_out_of_one_database_and_puts_it_in_another() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"RPUSH", b"l", b"a", b"b"]), ":2\r\n");
assert_eq!(f.run(&[b"MOVE", b"l", b"1"]), ":1\r\n");
assert_eq!(f.run(&[b"EXISTS", b"l"]), ":0\r\n");
assert_eq!(f.run(&[b"SELECT", b"1"]), "+OK\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"l", b"0", b"-1"]),
"*2\r\n$1\r\na\r\n$1\r\nb\r\n"
);
assert_eq!(f.run(&[b"MOVE", b"l", b"0"]), ":1\r\n");
assert_eq!(f.run(&[b"EXISTS", b"l"]), ":0\r\n");
}
#[test]
fn move_answers_zero_when_either_end_says_no() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"MOVE", b"nope", b"1"]), ":0\r\n");
assert_eq!(f.run(&[b"SET", b"a", b"here"]), "+OK\r\n");
assert_eq!(f.run(&[b"SELECT", b"1"]), "+OK\r\n");
assert_eq!(f.run(&[b"SET", b"a", b"there"]), "+OK\r\n");
assert_eq!(f.run(&[b"SELECT", b"0"]), "+OK\r\n");
assert_eq!(f.run(&[b"MOVE", b"a", b"1"]), ":0\r\n");
assert_eq!(f.run(&[b"GET", b"a"]), "$4\r\nhere\r\n");
assert_eq!(f.run(&[b"SELECT", b"1"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"a"]), "$5\r\nthere\r\n");
}
#[test]
fn move_refuses_a_database_that_is_not_one_and_the_one_it_is_on() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"MOVE", b"a", b"0"]),
"-ERR source and destination objects are the same\r\n"
);
assert_eq!(
f.run(&[b"MOVE", b"a", b"99"]),
"-ERR DB index is out of range\r\n"
);
assert_eq!(
f.run(&[b"MOVE", b"a", b"-1"]),
"-ERR DB index is out of range\r\n"
);
assert_eq!(
f.run(&[b"MOVE", b"a", b"x"]),
"-ERR value is not an integer or out of range\r\n"
);
}
#[test]
fn swapdb_swaps_what_two_connections_would_see() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SET", b"k", b"zero"]), "+OK\r\n");
assert_eq!(f.run(&[b"SELECT", b"1"]), "+OK\r\n");
assert_eq!(f.run(&[b"SET", b"k", b"one"]), "+OK\r\n");
assert_eq!(f.run(&[b"SELECT", b"0"]), "+OK\r\n");
assert_eq!(f.run(&[b"SWAPDB", b"0", b"1"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"k"]), "$3\r\none\r\n");
assert_eq!(f.run(&[b"SELECT", b"1"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"k"]), "$4\r\nzero\r\n");
assert_eq!(f.run(&[b"SWAPDB", b"1", b"1"]), "+OK\r\n");
assert_eq!(f.run(&[b"GET", b"k"]), "$4\r\nzero\r\n");
}
#[test]
fn swapdb_says_which_index_it_could_not_read() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"SWAPDB", b"x", b"1"]),
"-ERR invalid first DB index\r\n"
);
assert_eq!(
f.run(&[b"SWAPDB", b"0", b"y"]),
"-ERR invalid second DB index\r\n"
);
assert_eq!(
f.run(&[b"SWAPDB", b"99999999999999", b"1"]),
"-ERR invalid first DB index\r\n"
);
assert_eq!(
f.run(&[b"SWAPDB", b"0", b"99"]),
"-ERR DB index is out of range\r\n"
);
assert_eq!(
f.run(&[b"SWAPDB", b"-1", b"0"]),
"-ERR DB index is out of range\r\n"
);
}
#[test]
fn wait_answers_zero_replicas_without_waiting() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SET", b"a", b"v"]), "+OK\r\n");
assert_eq!(f.run(&[b"WAIT", b"0", b"0"]), ":0\r\n");
assert_eq!(f.run(&[b"WAIT", b"3", b"1000"]), ":0\r\n");
assert_eq!(f.run(&[b"WAIT", b"-1", b"0"]), ":0\r\n");
assert_eq!(
f.run(&[b"WAIT", b"x", b"0"]),
"-ERR value is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"WAIT", b"0", b"-1"]),
"-ERR timeout is negative\r\n"
);
assert_eq!(
f.run(&[b"WAIT", b"0", b"1.5"]),
"-ERR timeout is not an integer or out of range\r\n"
);
}
#[test]
fn waitaof_answers_two_zeroes_and_refuses_a_local_wait() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"WAITAOF", b"0", b"0", b"0"]), "*2\r\n:0\r\n:0\r\n");
assert_eq!(
f.run(&[b"WAITAOF", b"1", b"0", b"0"]),
"-ERR WAITAOF cannot be used when numlocal is set but appendonly is disabled.\r\n"
);
assert_eq!(
f.run(&[b"WAITAOF", b"2", b"0", b"0"]),
"-ERR value is out of range, value must between 0 and 1\r\n"
);
assert_eq!(
f.run(&[b"WAITAOF", b"0", b"-1", b"0"]),
"-ERR value is out of range, must be positive\r\n"
);
assert_eq!(
f.run(&[b"WAITAOF", b"1", b"0", b"-5"]),
"-ERR timeout is negative\r\n"
);
}
fn payload(reply: &[u8]) -> Vec<u8> {
let head = reply.windows(2).position(|w| w == b"\r\n").unwrap();
reply[head + 2..reply.len() - 2].to_vec()
}
#[test]
fn a_value_survives_a_dump_and_a_restore() {
let mut f = Fixture::new();
f.run(&[b"SET", b"s", b"hello"]);
f.run(&[b"RPUSH", b"l", b"a", b"b", b"c"]);
f.run(&[b"SADD", b"t", b"1", b"2", b"3"]);
f.run(&[b"SADD", b"u", b"x", b"y"]);
f.run(&[b"HSET", b"h", b"f", b"1", b"g", b"2"]);
f.run(&[b"ZADD", b"z", b"1.5", b"a", b"2.5", b"b"]);
for key in [&b"s"[..], b"l", b"t", b"u", b"h", b"z"] {
let mut copy = key.to_vec();
copy.push(b'2');
let bytes = payload(&f.raw(&[b"DUMP", key]));
assert_eq!(f.run(&[b"RESTORE", ©, b"0", &bytes]), "+OK\r\n");
assert_eq!(f.run(&[b"TYPE", ©]), f.run(&[b"TYPE", key]));
}
assert_eq!(f.run(&[b"GET", b"s2"]), "$5\r\nhello\r\n");
assert_eq!(
f.run(&[b"LRANGE", b"l2", b"0", b"-1"]),
"*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n"
);
assert_eq!(sorted(&f.run(&[b"SMEMBERS", b"t2"])), ["1", "2", "3"]);
assert_eq!(sorted(&f.run(&[b"SMEMBERS", b"u2"])), ["x", "y"]);
assert_eq!(f.run(&[b"HGET", b"h2", b"g"]), "$1\r\n2\r\n");
assert_eq!(f.run(&[b"ZSCORE", b"z2", b"b"]), "$3\r\n2.5\r\n");
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING", b"t2"]),
f.run(&[b"OBJECT", b"ENCODING", b"t"])
);
}
#[test]
fn a_dumped_hash_keeps_its_field_deadlines() {
let mut f = Fixture::new();
f.run(&[b"HSET", b"h", b"keep", b"1", b"go", b"2"]);
assert_eq!(
f.run(&[b"HEXPIRE", b"h", b"100", b"FIELDS", b"1", b"go"]),
"*1\r\n:1\r\n"
);
let bytes = payload(&f.raw(&[b"DUMP", b"h"]));
assert_eq!(f.run(&[b"RESTORE", b"h2", b"0", &bytes]), "+OK\r\n");
assert_eq!(
f.run(&[b"HTTL", b"h2", b"FIELDS", b"2", b"keep", b"go"]),
"*2\r\n:-1\r\n:100\r\n"
);
}
#[test]
fn dump_leaves_the_deadline_behind_and_restore_is_given_a_new_one() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"v", b"EX", b"100"]);
let bytes = payload(&f.raw(&[b"DUMP", b"a"]));
assert_eq!(f.run(&[b"RESTORE", b"b", b"0", &bytes]), "+OK\r\n");
assert_eq!(f.run(&[b"TTL", b"b"]), ":-1\r\n");
assert_eq!(f.run(&[b"RESTORE", b"c", b"5000", &bytes]), "+OK\r\n");
assert_eq!(f.run(&[b"TTL", b"c"]), ":5\r\n");
assert_eq!(
f.run(&[b"RESTORE", b"d", b"1", &bytes, b"ABSTTL"]),
"+OK\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"d"]), ":0\r\n");
}
#[test]
fn dump_answers_nothing_for_a_key_that_is_not_there() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"DUMP", b"nope"]), "$-1\r\n");
f.run(&[b"SET", b"gone", b"v", b"PX", b"10"]);
f.advance(50);
assert_eq!(f.run(&[b"DUMP", b"gone"]), "$-1\r\n");
}
#[test]
fn restore_refuses_a_key_that_is_there_unless_it_is_told_to_replace() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"first"]);
f.run(&[b"SET", b"b", b"second"]);
let bytes = payload(&f.raw(&[b"DUMP", b"b"]));
assert_eq!(
f.run(&[b"RESTORE", b"a", b"0", &bytes]),
"-BUSYKEY Target key name already exists.\r\n"
);
assert_eq!(f.run(&[b"GET", b"a"]), "$5\r\nfirst\r\n");
assert_eq!(
f.run(&[b"RESTORE", b"a", b"0", &bytes, b"REPLACE"]),
"+OK\r\n"
);
assert_eq!(f.run(&[b"GET", b"a"]), "$6\r\nsecond\r\n");
}
#[test]
fn restore_asks_about_the_key_before_it_looks_at_the_bytes() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"v"]);
assert_eq!(
f.run(&[b"RESTORE", b"a", b"0", b"rubbish"]),
"-BUSYKEY Target key name already exists.\r\n"
);
assert_eq!(
f.run(&[b"RESTORE", b"a", b"0", b"rubbish", b"FREQ", b"300"]),
"-ERR Invalid FREQ value, must be >= 0 and <= 255\r\n"
);
}
#[test]
fn restore_can_tell_a_bad_footer_from_bad_bytes() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"hello"]);
let good = payload(&f.raw(&[b"DUMP", b"a"]));
let mut flipped = good.clone();
flipped[2] ^= 0x40;
assert_eq!(
f.run(&[b"RESTORE", b"b", b"0", &flipped]),
"-ERR DUMP payload version or checksum are wrong\r\n"
);
assert_eq!(
f.run(&[b"RESTORE", b"b", b"0", b"short"]),
"-ERR DUMP payload version or checksum are wrong\r\n"
);
let mut truncated = good[..1].to_vec();
truncated.extend_from_slice(&good[good.len() - 10..good.len() - 8]);
let crc = yo_common::crc::crc64(0, &truncated);
truncated.extend_from_slice(&crc.to_le_bytes());
assert_eq!(
f.run(&[b"RESTORE", b"b", b"0", &truncated]),
"-ERR Bad data format\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"b"]), ":0\r\n");
}
#[test]
fn restore_checks_the_three_numbers_a_client_can_get_wrong() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"v"]);
let bytes = payload(&f.raw(&[b"DUMP", b"a"]));
assert_eq!(
f.run(&[b"RESTORE", b"b", b"-1", &bytes]),
"-ERR Invalid TTL value, must be >= 0\r\n"
);
assert_eq!(
f.run(&[b"RESTORE", b"b", b"0", &bytes, b"IDLETIME", b"-1"]),
"-ERR Invalid IDLETIME value, must be >= 0\r\n"
);
assert_eq!(
f.run(&[b"RESTORE", b"b", b"0", &bytes, b"FREQ", b"256"]),
"-ERR Invalid FREQ value, must be >= 0 and <= 255\r\n"
);
assert_eq!(
f.run(&[b"RESTORE", b"b", b"0", &bytes, b"IDLETIME", b"90"]),
"+OK\r\n"
);
assert_eq!(
f.run(&[b"RESTORE", b"c", b"0", &bytes, b"FREQ", b"200", b"REPLACE"]),
"+OK\r\n"
);
}
#[test]
fn restore_takes_idletime_or_freq_and_not_both() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"v"]);
let bytes = payload(&f.raw(&[b"DUMP", b"a"]));
assert_eq!(
f.run(&[
b"RESTORE",
b"b",
b"0",
&bytes,
b"IDLETIME",
b"1",
b"FREQ",
b"2"
]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[
b"RESTORE",
b"b",
b"0",
&bytes,
b"FREQ",
b"2",
b"IDLETIME",
b"1"
]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"RESTORE", b"b", b"0", &bytes, b"FREQ"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"RESTORE", b"b", b"0", &bytes, b"NOSUCH"]),
"-ERR syntax error\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("*6\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 the_eviction_policy_reads_back_what_was_written_to_it() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"maxmemory-policy"]),
"*2\r\n$16\r\nmaxmemory-policy\r\n$10\r\nnoeviction\r\n"
);
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"maxmemory-policy", b"AllKeys-LFU"]),
"+OK\r\n",
"the name is matched without regard to case, like every other one"
);
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"maxmemory-policy"]),
"*2\r\n$16\r\nmaxmemory-policy\r\n$11\r\nallkeys-lfu\r\n"
);
assert!(
f.run(&[b"INFO", b"memory"])
.contains("maxmemory_policy:allkeys-lfu"),
"INFO and CONFIG disagree about the policy"
);
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"maxmemory-policy", b"garbage"]),
"-ERR CONFIG SET failed (possibly related to argument 'maxmemory-policy') - argument(s) must be one of the following: volatile-lru, volatile-lfu, volatile-random, volatile-ttl, volatile-lrm, allkeys-lru, allkeys-lfu, allkeys-random, allkeys-lrm, noeviction\r\n"
);
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"maxmemory-policy"]),
"*2\r\n$16\r\nmaxmemory-policy\r\n$11\r\nallkeys-lfu\r\n"
);
f.run(&[
b"CONFIG",
b"SET",
b"hash-max-listpack-entries",
b"7",
b"maxmemory-policy",
b"nonsense",
]);
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"
);
}
#[test]
fn the_three_eviction_numbers_read_back_too() {
let mut f = Fixture::new();
for (name, default, set) in [
("maxmemory-samples", "5", "12"),
("lfu-log-factor", "10", "3"),
("lfu-decay-time", "1", "60"),
] {
let get = || {
format!(
"*2\r\n${}\r\n{name}\r\n${}\r\n{default}\r\n",
name.len(),
default.len()
)
};
assert_eq!(f.run(&[b"CONFIG", b"GET", name.as_bytes()]), get());
assert_eq!(
f.run(&[b"CONFIG", b"SET", name.as_bytes(), set.as_bytes()]),
"+OK\r\n"
);
assert_eq!(
f.run(&[b"CONFIG", b"GET", name.as_bytes()]),
format!(
"*2\r\n${}\r\n{name}\r\n${}\r\n{set}\r\n",
name.len(),
set.len()
)
);
assert_eq!(
f.run(&[b"CONFIG", b"SET", name.as_bytes(), b"soon"]),
format!(
"-ERR CONFIG SET failed (possibly related to argument '{name}') - argument couldn't be parsed into an integer\r\n"
)
);
}
}
#[test]
fn the_memory_limit_reads_back_in_bytes_whatever_the_unit_was() {
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",
"no limit is the default"
);
for (typed, bytes) in [
(&b"1024"[..], "1024"),
(b"1k", "1000"),
(b"1kb", "1024"),
(b"1M", "1000000"),
(b"1Mb", "1048576"),
(b"1gb", "1073741824"),
(b"100mb", "104857600"),
] {
assert_eq!(f.run(&[b"CONFIG", b"SET", b"maxmemory", typed]), "+OK\r\n");
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"maxmemory"]),
format!("*2\r\n$9\r\nmaxmemory\r\n${}\r\n{bytes}\r\n", bytes.len()),
"set {}",
String::from_utf8_lossy(typed)
);
}
assert!(
f.run(&[b"INFO", b"memory"]).contains("maxmemory:104857600"),
"the report agrees with the setting"
);
for bad in [&b"1tb"[..], b"-1", b"", b"lots"] {
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"maxmemory", bad]),
"-ERR CONFIG SET failed (possibly related to argument 'maxmemory') - argument must be a memory value\r\n",
"refused {}",
String::from_utf8_lossy(bad)
);
}
assert!(
f.run(&[b"INFO", b"memory"]).contains("maxmemory:104857600"),
"and the refusal left the old one alone"
);
}
#[test]
fn a_write_is_refused_when_there_is_no_room_and_nothing_to_evict() {
let mut f = Fixture::new();
f.run(&[b"SET", b"here", b"already"]);
f.run(&[b"CONFIG", b"SET", b"maxmemory", b"1"]);
assert_eq!(
f.run(&[b"SET", b"k", b"v"]),
"-OOM command not allowed when used memory > 'maxmemory'.\r\n"
);
assert_eq!(
f.run(&[b"LPUSH", b"l", b"v"]),
"-OOM command not allowed when used memory > 'maxmemory'.\r\n"
);
assert_eq!(f.run(&[b"GET", b"here"]), "$7\r\nalready\r\n");
assert_eq!(f.run(&[b"DEL", b"here"]), ":1\r\n");
assert!(f.run(&[b"INFO", b"stats"]).contains("evicted_keys:0"));
f.run(&[b"CONFIG", b"SET", b"maxmemory", b"0"]);
assert_eq!(f.run(&[b"SET", b"k", b"v"]), "+OK\r\n");
}
#[test]
fn an_allkeys_policy_makes_room_instead_of_refusing() {
let mut f = Fixture::new();
let val = vec![b'v'; 256];
for i in 0..24000u32 {
let k = format!("key:{i:08}");
f.run(&[b"SET", k.as_bytes(), &val]);
}
let full = f.server.memory_bytes();
assert!(
full > 3 * 1024 * 1024,
"the arena is several segments: {full}"
);
let limit = full - 2 * 1024 * 1024;
f.run(&[b"CONFIG", b"SET", b"maxmemory-policy", b"allkeys-lru"]);
f.run(&[
b"CONFIG",
b"SET",
b"maxmemory",
limit.to_string().as_bytes(),
]);
for i in 0..2000u32 {
let k = format!("new:{i:08}");
assert_eq!(
f.run(&[b"SET", k.as_bytes(), &val]),
"+OK\r\n",
"write {i} was refused"
);
f.server.refresh_memory();
if f.server.memory_bytes() <= limit {
break;
}
}
assert!(
f.server.memory_bytes() <= limit,
"it never got under: {} against {limit}",
f.server.memory_bytes()
);
let info = f.run(&[b"INFO", b"stats"]);
assert!(!info.contains("evicted_keys:0"), "{info}");
assert!(
f.run(&[b"DBSIZE"]) != ":0\r\n",
"and it did not empty the database to get there"
);
}
#[test]
fn the_running_total_and_the_walk_agree_on_a_mixed_keyspace() {
let mut f = Fixture::new();
f.run(&[b"CONFIG", b"SET", b"maxmemory", b"1gb"]);
let big = vec![b'v'; 200];
for i in 0..400u32 {
let n = i.to_string();
let n = n.as_bytes();
f.run(&[b"SADD", b"s", n]);
f.run(&[b"SADD", b"s2", &big]);
f.run(&[b"HSET", b"h", n, &big]);
f.run(&[b"RPUSH", b"l", &big]);
f.run(&[b"ZADD", b"z", n, n]);
f.run(&[b"ARSET", b"a", n, &big]);
if i % 7 == 0 {
f.run(&[b"SREM", b"s", n]);
f.run(&[b"HDEL", b"h", n]);
f.run(&[b"LPOP", b"l"]);
f.run(&[b"ZREM", b"z", n]);
f.run(&[b"ARDEL", b"a", n]);
}
if i % 53 == 0 {
f.run(&[b"DEL", b"s2"]);
}
assert_eq!(
f.server.settled_memory(),
f.server.memory_bytes(),
"after round {i}"
);
}
assert_eq!(f.run(&[b"DBSIZE"]), ":6\r\n");
assert!(
f.server.memory_bytes() > 512 * 1024,
"{}",
f.server.memory_bytes()
);
f.run(&[b"FLUSHALL"]);
assert_eq!(f.server.settled_memory(), f.server.memory_bytes());
}
#[test]
fn taking_the_limit_away_stops_the_counting_and_putting_it_back_starts_again() {
let mut f = Fixture::new();
for i in 0..200u32 {
let n = i.to_string();
f.run(&[b"SADD", b"s", n.as_bytes()]);
f.run(&[b"HSET", b"h", n.as_bytes(), b"value"]);
}
f.run(&[b"CONFIG", b"SET", b"maxmemory", b"1gb"]);
assert_eq!(f.server.settled_memory(), f.server.memory_bytes());
f.run(&[b"CONFIG", b"SET", b"maxmemory", b"0"]);
for i in 200..400u32 {
let n = i.to_string();
f.run(&[b"SADD", b"s", n.as_bytes()]);
}
f.run(&[b"CONFIG", b"SET", b"maxmemory", b"1gb"]);
assert_eq!(
f.server.settled_memory(),
f.server.memory_bytes(),
"the writes it was not watching are in the number it started from"
);
}
#[test]
fn evicted_keys_and_expired_keys_are_different_numbers() {
let mut f = Fixture::new();
f.run(&[b"SET", b"gone", b"v", b"PX", b"1"]);
f.server.db(0).clock_mut().advance(20);
f.run(&[b"GET", b"gone"]);
let info = f.run(&[b"INFO", b"stats"]);
assert!(info.contains("expired_keys:1"), "{info}");
assert!(info.contains("evicted_keys:0"), "{info}");
}
#[test]
fn the_object_subcommands_follow_the_policy() {
let mut f = Fixture::new();
f.run(&[b"SET", b"s", b"v"]);
assert_eq!(f.run(&[b"OBJECT", b"IDLETIME", b"s"]), ":0\r\n");
assert!(
f.run(&[b"OBJECT", b"FREQ", b"s"])
.starts_with("-ERR An LFU maxmemory policy is not selected"),
);
f.run(&[b"CONFIG", b"SET", b"maxmemory-policy", b"allkeys-lfu"]);
assert!(
f.run(&[b"OBJECT", b"IDLETIME", b"s"])
.starts_with("-ERR An LFU maxmemory policy is selected"),
);
assert!(
f.run(&[b"OBJECT", b"FREQ", b"s"]).starts_with(':'),
"FREQ should answer under an LFU policy"
);
}
#[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");
}
#[test]
fn commandstats_is_asked_for_and_replication_is_not() {
let mut f = Fixture::new();
for arg in ["", "all", "default", "everything"] {
let info = if arg.is_empty() {
f.run(&[b"INFO"])
} else {
f.run(&[b"INFO", arg.as_bytes()])
};
assert!(info.contains("redis_version"), "{arg}: {info}");
assert!(info.contains("used_cpu_user"), "{arg}: {info}");
assert!(info.contains("used_memory"), "{arg}: {info}");
assert!(!info.contains("sentinel_tilt"), "{arg}: {info}");
let asked = arg == "all" || arg == "everything";
assert_eq!(
info.contains("rejected_calls"),
asked,
"{arg} should{} carry the command counters: {info}",
if asked { "" } else { " not" }
);
}
let cpu = f.run(&[b"INFO", b"cpu"]);
assert!(cpu.contains("used_cpu_user"), "{cpu}");
assert!(!cpu.contains("used_memory"), "{cpu}");
let stats = f.run(&[b"INFO", b"commandSTATS"]);
assert!(!stats.contains("used_memory"), "{stats}");
assert!(stats.contains("rejected_calls"), "{stats}");
let pair = f.run(&[b"INFO", b"cpu", b"sentinel"]);
assert!(pair.contains("used_cpu_user"), "{pair}");
assert!(!pair.contains("master_repl_offset"), "{pair}");
let with_all = f.run(&[b"INFO", b"cpu", b"all"]);
assert!(with_all.contains("used_memory"), "{with_all}");
assert!(with_all.contains("master_repl_offset"), "{with_all}");
assert!(with_all.contains("rejected_calls"), "{with_all}");
assert_eq!(
with_all.matches("used_cpu_user_children").count(),
1,
"{with_all}"
);
let with_default = f.run(&[b"INFO", b"cpu", b"default"]);
assert!(with_default.contains("used_memory"), "{with_default}");
assert!(
with_default.contains("master_repl_offset"),
"{with_default}"
);
assert!(!with_default.contains("rejected_calls"), "{with_default}");
assert_eq!(
with_default.matches("used_cpu_user_children").count(),
1,
"{with_default}"
);
}
#[test]
fn info_memory_reports_the_cap_and_the_quarter_of_it_that_gets_used() {
let mut f = Fixture::new();
let info = f.run(&[b"INFO", b"memory"]);
for field in [
"total_system_memory:",
"mem_cgroup_limit:",
"mem_limit:",
"mem_budget:",
] {
assert!(info.contains(field), "no {field} in {info}");
}
let field = |name: &str| -> u64 {
info.lines()
.find_map(|l| l.strip_prefix(name))
.unwrap_or_else(|| panic!("no {name} in {info}"))
.trim()
.parse()
.unwrap_or_else(|_| panic!("{name} is not a number in {info}"))
};
let limit = field("mem_limit:");
assert_eq!(field("mem_budget:"), limit / 4, "{info}");
if limit != 0 {
let host = field("total_system_memory:");
let cgroup = field("mem_cgroup_limit:");
assert!(
limit == host || limit == cgroup,
"the limit came from neither number: {info}"
);
}
}
#[test]
fn a_command_counts_what_it_did_separately_from_what_it_refused() {
let mut f = Fixture::new();
f.run(&[b"SET", b"k", b"v"]);
f.run(&[b"SET", b"k", b"w"]);
f.run(&[b"LPUSH", b"k", b"x"]);
f.run(&[b"LPUSH", b"k"]);
let stats = f.run(&[b"INFO", b"commandstats"]);
assert!(
stats.contains("cmdstat_set:calls=2,rejected_calls=0,failed_calls=0"),
"{stats}"
);
assert!(
stats.contains("cmdstat_lpush:calls=1,rejected_calls=1,failed_calls=1"),
"{stats}"
);
assert!(
!stats.contains("cmdstat_zadd"),
"a command nobody has sent has no row: {stats}"
);
}
#[test]
fn the_active_sweep_reclaims_keys_no_client_comes_back_for() {
let mut f = Fixture::new();
for i in 0..3_000u32 {
f.run(&[b"SET", format!("d{i}").as_bytes(), b"v", b"PX", b"50"]);
}
for i in 0..1_000u32 {
f.run(&[b"SET", format!("k{i}").as_bytes(), b"v"]);
}
assert_eq!(f.run(&[b"DBSIZE"]), ":4000\r\n");
f.advance(100);
assert_eq!(
f.run(&[b"DBSIZE"]),
":4000\r\n",
"DBSIZE counts records and nothing has read past the dead ones yet"
);
let mut spent = 0;
for _ in 0..2_000 {
spent += f.server.expire_step(4096);
if f.run(&[b"DBSIZE"]) == ":1000\r\n" {
break;
}
}
assert_eq!(f.run(&[b"DBSIZE"]), ":1000\r\n", "spent {spent} looks");
assert!(f.run(&[b"INFO", b"stats"]).contains("expired_keys:3000"));
for i in 0..1_000u32 {
assert_eq!(
f.run(&[b"GET", format!("k{i}").as_bytes()]),
"$1\r\nv\r\n",
"it took a key that had no deadline"
);
}
}
#[test]
fn a_sweep_of_a_server_with_no_deadlines_anywhere_costs_nothing() {
let mut f = Fixture::new();
for i in 0..2_000u32 {
f.run(&[b"SET", format!("k{i}").as_bytes(), b"v"]);
}
assert_eq!(f.server.expire_step(4096), 0);
f.run(&[b"SELECT", b"3"]);
f.run(&[b"SET", b"x", b"v", b"PX", b"50"]);
f.advance(100);
for _ in 0..64 {
f.server.expire_step(4096);
}
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
f.run(&[b"SELECT", b"0"]);
assert_eq!(f.run(&[b"DBSIZE"]), ":2000\r\n");
assert_eq!(f.server.expire_step(4096), 0, "and it is quiet again");
}
#[test]
fn the_sweep_the_loop_calls_runs_at_most_once_a_millisecond() {
let mut f = Fixture::new();
for i in 0..500u32 {
f.run(&[b"SET", format!("d{i}").as_bytes(), b"v", b"PX", b"50"]);
}
f.advance(100);
let at = f.server.db(0).clock().now_ms();
f.server.set_clock_ms(at);
assert!(f.server.expire_slice(8) > 0, "the first one works");
for _ in 0..1_000 {
assert_eq!(
f.server.expire_slice(8),
0,
"the millisecond has not moved and neither should this"
);
}
assert!(
f.server.db(0).expires() > 400,
"there is plenty left to take"
);
f.server.set_clock_ms(at + 1);
assert!(f.server.expire_slice(8) > 0, "and then it goes again");
}
#[test]
fn info_keyspace_counts_the_keys_that_have_a_deadline() {
let mut f = Fixture::new();
f.run(&[b"MSET", b"a", b"1", b"b", b"2", b"c", b"3"]);
assert!(
f.run(&[b"INFO", b"keyspace"])
.contains("db0:keys=3,expires=0"),
"none of them has one yet"
);
f.run(&[b"EXPIRE", b"a", b"1000"]);
f.run(&[b"EXPIRE", b"b", b"1000"]);
let two = f.run(&[b"INFO", b"keyspace"]);
assert!(two.contains("db0:keys=3,expires=2"), "{two}");
f.run(&[b"PERSIST", b"a"]);
f.run(&[b"DEL", b"b"]);
let none = f.run(&[b"INFO", b"keyspace"]);
assert!(none.contains("db0:keys=2,expires=0"), "{none}");
f.run(&[b"SELECT", b"1"]);
f.run(&[b"SET", b"x", b"1", b"EX", b"1000"]);
let both = f.run(&[b"INFO", b"keyspace"]);
assert!(both.contains("db0:keys=2,expires=0"), "{both}");
assert!(both.contains("db1:keys=1,expires=1"), "{both}");
}
#[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()
);
}
#[test]
fn a_bit_is_written_and_read_back_and_a_write_unpacks_an_int() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"SETBIT", b"k", b"7", b"1"]), ":0\r\n");
assert_eq!(f.run(&[b"GET", b"k"]), "$1\r\n\u{1}\r\n");
assert_eq!(f.run(&[b"GETBIT", b"k", b"7"]), ":1\r\n");
assert_eq!(f.run(&[b"GETBIT", b"k", b"6"]), ":0\r\n");
assert_eq!(f.run(&[b"GETBIT", b"k", b"100"]), ":0\r\n");
assert_eq!(f.run(&[b"SETBIT", b"k", b"7", b"0"]), ":1\r\n");
assert_eq!(f.run(&[b"SETBIT", b"nk", b"0", b"0"]), ":0\r\n");
assert_eq!(f.run(&[b"STRLEN", b"nk"]), ":1\r\n");
assert_eq!(f.run(&[b"OBJECT", b"ENCODING", b"nk"]), "$3\r\nraw\r\n");
f.run(&[b"SET", b"num", b"12345"]);
assert_eq!(f.run(&[b"GETBIT", b"num", b"1"]), ":0\r\n");
assert_eq!(f.run(&[b"OBJECT", b"ENCODING", b"num"]), "$3\r\nint\r\n");
assert_eq!(f.run(&[b"SETBIT", b"num", b"1", b"1"]), ":0\r\n");
assert_eq!(f.run(&[b"OBJECT", b"ENCODING", b"num"]), "$3\r\nraw\r\n");
assert_eq!(f.run(&[b"GET", b"num"]), "$5\r\nq2345\r\n");
}
#[test]
fn bits_are_counted_over_a_range_of_bytes_or_of_bits() {
let mut f = Fixture::new();
f.run(&[b"SET", b"mykey", b"foobar"]);
assert_eq!(f.run(&[b"BITCOUNT", b"mykey"]), ":26\r\n");
assert_eq!(f.run(&[b"BITCOUNT", b"mykey", b"0", b"0"]), ":4\r\n");
assert_eq!(f.run(&[b"BITCOUNT", b"mykey", b"1", b"1"]), ":6\r\n");
assert_eq!(
f.run(&[b"BITCOUNT", b"mykey", b"1", b"1", b"BYTE"]),
":6\r\n"
);
assert_eq!(
f.run(&[b"BITCOUNT", b"mykey", b"0", b"-5", b"BIT"]),
":25\r\n"
);
assert_eq!(
f.run(&[b"BITCOUNT", b"mykey", b"5", b"30", b"BIT"]),
":17\r\n"
);
assert_eq!(f.run(&[b"BITCOUNT", b"nokey"]), ":0\r\n");
assert_eq!(f.run(&[b"BITCOUNT", b"mykey", b"10", b"20"]), ":0\r\n");
assert_eq!(
f.run(&[b"BITCOUNT", b"mykey", b"0"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"BITCOUNT", b"mykey", b"0", b"1", b"NIB"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_search_for_a_nought_past_the_end_answers_the_length_in_bits() {
let mut f = Fixture::new();
f.run(&[b"SET", b"ones", b"\xff\xff\xff"]);
assert_eq!(f.run(&[b"BITPOS", b"ones", b"0"]), ":24\r\n");
assert_eq!(f.run(&[b"BITPOS", b"ones", b"0", b"0"]), ":24\r\n");
assert_eq!(f.run(&[b"BITPOS", b"ones", b"0", b"0", b"-1"]), ":-1\r\n");
assert_eq!(f.run(&[b"BITPOS", b"ones", b"0", b"0", b"3"]), ":-1\r\n");
assert_eq!(f.run(&[b"BITPOS", b"ones", b"1"]), ":0\r\n");
f.run(&[b"SET", b"mid", b"\x00\xff\xf0"]);
assert_eq!(f.run(&[b"BITPOS", b"mid", b"1", b"0"]), ":8\r\n");
assert_eq!(f.run(&[b"BITPOS", b"mid", b"1", b"2"]), ":16\r\n");
assert_eq!(
f.run(&[b"BITPOS", b"mid", b"1", b"0", b"-1", b"BIT"]),
":8\r\n"
);
assert_eq!(f.run(&[b"BITPOS", b"gone", b"1"]), ":-1\r\n");
assert_eq!(f.run(&[b"BITPOS", b"gone", b"0"]), ":0\r\n");
}
#[test]
fn the_eight_combinations_write_what_a_real_server_writes() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"abc"]);
f.run(&[b"SET", b"b", b"abd"]);
let cases: &[(&[u8], &str)] = &[
(b"AND", "ab`"),
(b"OR", "abg"),
(b"XOR", "\u{0}\u{0}\u{7}"),
(b"DIFF", "\u{0}\u{0}\u{3}"),
(b"DIFF1", "\u{0}\u{0}\u{4}"),
(b"ANDOR", "ab`"),
(b"ONE", "\u{0}\u{0}\u{7}"),
];
for (op, want) in cases {
assert_eq!(f.run(&[b"BITOP", op, b"d", b"a", b"b"]), ":3\r\n", "{op:?}");
assert_eq!(
f.run(&[b"GET", b"d"]),
format!("$3\r\n{want}\r\n"),
"{op:?}"
);
}
assert_eq!(f.run(&[b"BITOP", b"NOT", b"d", b"a"]), ":3\r\n");
assert_eq!(f.raw(&[b"GET", b"d"]), b"$3\r\n\x9e\x9d\x9c\r\n".to_vec());
assert_eq!(f.run(&[b"BITOP", b"AND", b"d", b"a", b"gone"]), ":3\r\n");
assert_eq!(f.run(&[b"GET", b"d"]), "$3\r\n\u{0}\u{0}\u{0}\r\n");
f.run(&[b"SET", b"dest", b"x"]);
assert_eq!(f.run(&[b"BITOP", b"AND", b"dest", b"g1", b"g2"]), ":0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"dest"]), ":0\r\n");
}
#[test]
fn bitop_names_the_operation_in_its_own_complaints() {
let mut f = Fixture::new();
f.run(&[b"SET", b"a", b"abc"]);
assert_eq!(
f.run(&[b"BITOP", b"nope", b"d", b"a"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"BITOP", b"NOT", b"d", b"a", b"a"]),
"-ERR BITOP NOT must be called with a single source key.\r\n"
);
for op in [&b"DIFF"[..], b"DIFF1", b"ANDOR"] {
assert_eq!(
f.run(&[b"BITOP", op, b"d", b"a"]),
format!(
"-ERR BITOP {} must be called with at least two source keys.\r\n",
String::from_utf8_lossy(op)
)
);
}
f.run(&[b"LPUSH", b"l", b"x"]);
assert_eq!(
f.run(&[b"BITOP", b"AND", b"d", b"a", b"l"]),
"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"
);
}
#[test]
fn bitfield_reads_and_writes_packed_fields() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"BITFIELD", b"bf"]), "*0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"bf"]), ":0\r\n");
assert_eq!(
f.run(&[
b"BITFIELD",
b"bf",
b"INCRBY",
b"u2",
b"100",
b"1",
b"GET",
b"u4",
b"0"
]),
"*2\r\n:1\r\n:0\r\n"
);
assert_eq!(f.run(&[b"STRLEN", b"bf"]), ":13\r\n");
assert_eq!(
f.run(&[
b"BITFIELD",
b"bf",
b"SET",
b"u8",
b"#0",
b"255",
b"GET",
b"u8",
b"#0"
]),
"*2\r\n:0\r\n:255\r\n"
);
assert_eq!(
f.run(&[
b"BITFIELD",
b"bf",
b"OVERFLOW",
b"SAT",
b"INCRBY",
b"i8",
b"0",
b"120",
b"INCRBY",
b"i8",
b"0",
b"120"
]),
"*2\r\n:119\r\n:127\r\n"
);
assert_eq!(
f.run(&[
b"BITFIELD",
b"bf2",
b"OVERFLOW",
b"FAIL",
b"INCRBY",
b"u2",
b"0",
b"5"
]),
"*1\r\n$-1\r\n"
);
assert_eq!(
f.run(&[
b"BITFIELD",
b"bf3",
b"OVERFLOW",
b"WRAP",
b"INCRBY",
b"u2",
b"0",
b"5"
]),
"*1\r\n:1\r\n"
);
assert_eq!(
f.run(&[b"BITFIELD", b"bf3", b"GET", b"i64", b"0"]),
"*1\r\n:4611686018427387904\r\n"
);
}
#[test]
fn a_bad_bitfield_subcommand_leaves_the_key_alone() {
let mut f = Fixture::new();
let bad_type = "-ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is.\r\n";
assert_eq!(
f.run(&[
b"BITFIELD",
b"bad",
b"SET",
b"u8",
b"0",
b"1",
b"GET",
b"u99",
b"0"
]),
bad_type
);
assert_eq!(f.run(&[b"EXISTS", b"bad"]), ":0\r\n");
assert_eq!(
f.run(&[b"BITFIELD", b"bad", b"GET", b"u64", b"0"]),
bad_type
);
assert_eq!(
f.run(&[b"BITFIELD", b"bad", b"GET"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"BITFIELD", b"bad", b"NOPE", b"u8", b"0"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"BITFIELD", b"bad", b"OVERFLOW"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[
b"BITFIELD",
b"bad",
b"OVERFLOW",
b"NOPE",
b"GET",
b"u8",
b"0"
]),
"-ERR Invalid OVERFLOW type specified\r\n"
);
assert_eq!(
f.run(&[b"BITFIELD", b"bad", b"SET", b"u8", b"0", b"notanum"]),
"-ERR value is not an integer or out of range\r\n"
);
for at in [&b"#-1"[..], b"abc"] {
assert_eq!(
f.run(&[b"BITFIELD", b"bad", b"GET", b"u8", at]),
"-ERR bit offset is not an integer or out of range\r\n"
);
}
}
#[test]
fn bitfield_ro_answers_gets_and_refuses_the_rest() {
let mut f = Fixture::new();
f.run(&[b"SET", b"n", b"123"]);
assert_eq!(
f.run(&[b"BITFIELD_RO", b"n", b"GET", b"u8", b"0"]),
"*1\r\n:49\r\n"
);
assert_eq!(f.run(&[b"OBJECT", b"ENCODING", b"n"]), "$3\r\nint\r\n");
assert_eq!(
f.run(&[
b"BITFIELD_RO",
b"n",
b"OVERFLOW",
b"SAT",
b"GET",
b"u8",
b"0"
]),
"*1\r\n:49\r\n"
);
for sub in [&b"SET"[..], b"INCRBY"] {
assert_eq!(
f.run(&[b"BITFIELD_RO", b"n", sub, b"u8", b"0", b"1"]),
"-ERR BITFIELD_RO only supports the GET subcommand\r\n"
);
}
assert_eq!(
f.run(&[b"BITFIELD_RO", b"gone", b"GET", b"u8", b"100"]),
"*1\r\n:0\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"gone"]), ":0\r\n");
}
#[test]
fn an_offset_off_the_end_of_the_world_is_refused() {
let mut f = Fixture::new();
let bad = "-ERR bit offset is not an integer or out of range\r\n";
for arg in [&b"abc"[..], b"-1", b"4294967296"] {
assert_eq!(f.run(&[b"SETBIT", b"k", arg, b"1"]), bad);
assert_eq!(f.run(&[b"GETBIT", b"k", arg]), bad);
}
for arg in [&b"2"[..], b"-1"] {
assert_eq!(
f.run(&[b"BITPOS", b"k", arg]),
"-ERR The bit argument must be 1 or 0.\r\n"
);
}
assert_eq!(
f.run(&[b"BITPOS", b"k", b"abc"]),
"-ERR value is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"BITPOS", b"k", b"0", b"5", b"BIT"]),
"-ERR value is not an integer or out of range\r\n"
);
let bad_bit = "-ERR bit is not an integer or out of range\r\n";
assert_eq!(f.run(&[b"SETBIT", b"k", b"0", b"2"]), bad_bit);
assert_eq!(f.run(&[b"SETBIT", b"k", b"0", b"abc"]), bad_bit);
}
#[test]
fn every_bitmap_command_says_wrongtype() {
let mut f = Fixture::new();
f.run(&[b"LPUSH", b"l", b"x"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
let cases: &[&[&[u8]]] = &[
&[b"SETBIT", b"l", b"0", b"1"],
&[b"GETBIT", b"l", b"0"],
&[b"BITCOUNT", b"l"],
&[b"BITPOS", b"l", b"1"],
&[b"BITOP", b"AND", b"d", b"l"],
&[b"BITFIELD", b"l", b"GET", b"u8", b"0"],
&[b"BITFIELD_RO", b"l", b"GET", b"u8", b"0"],
];
for case in cases {
assert_eq!(f.run(case), wrong, "{:?}", case[0]);
}
}
#[test]
fn a_sketch_is_added_to_and_counted() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"PFADD", b"h"]), ":1\r\n");
assert_eq!(f.run(&[b"PFADD", b"h"]), ":0\r\n");
assert_eq!(f.run(&[b"PFCOUNT", b"h"]), ":0\r\n");
assert_eq!(f.run(&[b"STRLEN", b"h"]), ":18\r\n");
assert_eq!(f.run(&[b"TYPE", b"h"]), "+string\r\n");
assert_eq!(f.run(&[b"OBJECT", b"ENCODING", b"h"]), "$3\r\nraw\r\n");
assert_eq!(f.run(&[b"PFADD", b"h", b"a", b"b", b"c"]), ":1\r\n");
assert_eq!(f.run(&[b"PFADD", b"h", b"a"]), ":0\r\n");
assert_eq!(f.run(&[b"PFCOUNT", b"h"]), ":3\r\n");
}
#[test]
fn the_bytes_of_a_sketch_are_the_ones_a_real_server_writes() {
let mut f = Fixture::new();
f.run(&[b"PFADD", b"h", b"a", b"b", b"c"]);
let want = b"HYLL\x01\0\0\0\0\0\0\0\0\0\0\x80\x60\xf3\x80\x50\xb1\x84\x4b\xfb\x80\x42\x5a";
let mut reply = b"$27\r\n".to_vec();
reply.extend_from_slice(want);
reply.extend_from_slice(b"\r\n");
assert_eq!(f.raw(&[b"GET", b"h"]), reply);
}
#[test]
fn counting_several_keys_counts_their_union() {
let mut f = Fixture::new();
f.run(&[b"PFADD", b"a", b"x", b"y"]);
f.run(&[b"PFADD", b"b", b"y", b"z"]);
assert_eq!(f.run(&[b"PFCOUNT", b"a"]), ":2\r\n");
assert_eq!(f.run(&[b"PFCOUNT", b"a", b"b"]), ":3\r\n");
assert_eq!(f.run(&[b"PFCOUNT", b"gone"]), ":0\r\n");
assert_eq!(f.run(&[b"PFCOUNT", b"a", b"gone"]), ":2\r\n");
assert_eq!(f.run(&[b"EXISTS", b"gone"]), ":0\r\n");
}
#[test]
fn a_merge_keeps_what_the_destination_had() {
let mut f = Fixture::new();
f.run(&[b"PFADD", b"a", b"x", b"y"]);
f.run(&[b"PFADD", b"b", b"z"]);
assert_eq!(f.run(&[b"PFMERGE", b"d", b"a", b"b"]), "+OK\r\n");
assert_eq!(f.run(&[b"PFCOUNT", b"d"]), ":3\r\n");
f.run(&[b"PFADD", b"c", b"w"]);
assert_eq!(f.run(&[b"PFMERGE", b"d", b"c"]), "+OK\r\n");
assert_eq!(f.run(&[b"PFCOUNT", b"d"]), ":4\r\n");
assert_eq!(f.run(&[b"PFMERGE", b"fresh"]), "+OK\r\n");
assert_eq!(f.run(&[b"PFCOUNT", b"fresh"]), ":0\r\n");
}
#[test]
fn the_debug_forms_answer_four_different_shapes() {
let mut f = Fixture::new();
f.run(&[b"PFADD", b"h", b"a", b"b", b"c"]);
assert_eq!(f.run(&[b"PFDEBUG", b"ENCODING", b"h"]), "+sparse\r\n");
assert_eq!(
f.run(&[b"PFDEBUG", b"DECODE", b"h"]),
"$44\r\nZ:8436 v:1,1 Z:4274 v:2,1 Z:3068 v:1,1 Z:603\r\n"
);
assert_eq!(f.run(&[b"PFDEBUG", b"TODENSE", b"h"]), ":1\r\n");
assert_eq!(f.run(&[b"PFDEBUG", b"TODENSE", b"h"]), ":0\r\n");
assert_eq!(f.run(&[b"PFDEBUG", b"ENCODING", b"h"]), "+dense\r\n");
assert_eq!(f.run(&[b"STRLEN", b"h"]), ":12304\r\n");
assert_eq!(f.run(&[b"PFCOUNT", b"h"]), ":3\r\n");
assert_eq!(
f.run(&[b"PFDEBUG", b"DECODE", b"h"]),
"-ERR HLL encoding is not sparse\r\n"
);
let reply = f.run(&[b"PFDEBUG", b"GETREG", b"h"]);
assert!(reply.starts_with("*16384\r\n"), "{}", &reply[..16]);
assert_eq!(reply.matches(":0\r\n").count(), 16381);
assert_eq!(reply.matches(":1\r\n").count(), 2);
assert_eq!(reply.matches(":2\r\n").count(), 1);
assert_eq!(f.run(&[b"PFSELFTEST"]), "+OK\r\n");
}
#[test]
fn a_string_that_is_not_a_sketch_is_refused_with_its_own_sentence() {
let mut f = Fixture::new();
f.run(&[b"SET", b"plain", b"not a sketch"]);
let not_hll = "-WRONGTYPE Key is not a valid HyperLogLog string value.\r\n";
assert_eq!(f.run(&[b"PFADD", b"plain", b"a"]), not_hll);
assert_eq!(f.run(&[b"PFCOUNT", b"plain"]), not_hll);
assert_eq!(f.run(&[b"PFMERGE", b"plain"]), not_hll);
assert_eq!(f.run(&[b"PFDEBUG", b"ENCODING", b"plain"]), not_hll);
f.run(&[b"RPUSH", b"l", b"x"]);
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
assert_eq!(f.run(&[b"PFADD", b"l", b"a"]), wrong);
assert_eq!(f.run(&[b"PFCOUNT", b"l"]), wrong);
assert_eq!(f.run(&[b"PFMERGE", b"dest", b"l"]), wrong);
assert_eq!(f.run(&[b"EXISTS", b"dest"]), ":0\r\n");
assert_eq!(f.run(&[b"PFDEBUG", b"GETREG", b"l"]), wrong);
}
#[test]
fn pfdebug_has_its_own_complaints() {
let mut f = Fixture::new();
f.run(&[b"PFADD", b"h", b"a"]);
assert_eq!(
f.run(&[b"PFDEBUG", b"NOPE", b"h"]),
"-ERR Unknown PFDEBUG subcommand 'NOPE'\r\n"
);
let gone = "-ERR The specified key does not exist\r\n";
assert_eq!(f.run(&[b"PFDEBUG", b"GETREG", b"missing"]), gone);
assert_eq!(f.run(&[b"PFDEBUG", b"DECODE", b"missing"]), gone);
assert_eq!(f.run(&[b"PFDEBUG", b"ENCODING", b"missing"]), gone);
assert_eq!(f.run(&[b"PFDEBUG", b"TODENSE", b"missing"]), gone);
assert_eq!(
f.run(&[b"PFDEBUG"]),
"-ERR wrong number of arguments for 'pfdebug' command\r\n"
);
assert_eq!(
f.run(&[b"PFSELFTEST", b"x"]),
"-ERR wrong number of arguments for 'pfselftest' command\r\n"
);
}
#[test]
fn a_sketch_whose_opcodes_do_not_add_up_says_so() {
let mut f = Fixture::new();
f.run(&[b"PFADD", b"h", b"a", b"b", b"c"]);
let reply = f.raw(&[b"GET", b"h"]);
let short = reply[5..reply.len() - 3].to_vec();
f.run(&[b"SET", b"h", &short]);
assert_eq!(
f.run(&[b"PFCOUNT", b"h"]),
"-INVALIDOBJ Corrupted HLL object detected\r\n"
);
}
#[test]
fn a_sketch_survives_a_dump_and_a_restore_in_both_encodings() {
let mut f = Fixture::new();
f.run(&[b"PFADD", b"small", b"a", b"b", b"c"]);
for i in 0..10_000u32 {
let ele = format!("e{i}");
f.run(&[b"PFADD", b"big", ele.as_bytes()]);
}
assert_eq!(f.run(&[b"PFDEBUG", b"ENCODING", b"small"]), "+sparse\r\n");
assert_eq!(f.run(&[b"PFDEBUG", b"ENCODING", b"big"]), "+dense\r\n");
for key in [&b"small"[..], b"big"] {
let mut copy = key.to_vec();
copy.push(b'2');
let bytes = payload(&f.raw(&[b"DUMP", key]));
assert_eq!(f.run(&[b"RESTORE", ©, b"0", &bytes]), "+OK\r\n");
assert_eq!(f.raw(&[b"GET", ©]), f.raw(&[b"GET", key]));
assert_eq!(
f.run(&[b"PFDEBUG", b"ENCODING", ©]),
f.run(&[b"PFDEBUG", b"ENCODING", key])
);
assert_eq!(f.run(&[b"PFCOUNT", ©]), f.run(&[b"PFCOUNT", key]));
}
assert_eq!(f.run(&[b"PFCOUNT", b"small2"]), ":3\r\n");
assert_eq!(f.run(&[b"STRLEN", b"big2"]), ":12304\r\n");
}
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()
);
}
fn sicily(f: &mut Fixture) {
f.run(&[
b"GEOADD",
b"Sicily",
b"13.361389",
b"38.115556",
b"Palermo",
b"15.087269",
b"37.502669",
b"Catania",
]);
f.run(&[
b"GEOADD",
b"Sicily",
b"13.583333",
b"37.316667",
b"Agrigento",
]);
}
#[test]
fn places_go_in_as_scores_and_come_back_as_positions() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[
b"GEOADD",
b"Sicily",
b"13.361389",
b"38.115556",
b"Palermo",
b"15.087269",
b"37.502669",
b"Catania"
]),
":2\r\n"
);
assert_eq!(f.run(&[b"TYPE", b"Sicily"]), "+zset\r\n");
assert_eq!(
f.run(&[b"ZSCORE", b"Sicily", b"Palermo"]),
"$16\r\n3479099956230698\r\n"
);
assert_eq!(
f.run(&[b"GEOPOS", b"Sicily", b"Palermo", b"NonExisting"]),
"*2\r\n*2\r\n$18\r\n13.361389338970184\r\n$16\r\n38.1155563954963\r\n*-1\r\n"
);
assert_eq!(
f.run(&[
b"GEOHASH",
b"Sicily",
b"Palermo",
b"Catania",
b"NonExisting"
]),
"*3\r\n$11\r\nsqc8b49rny0\r\n$11\r\nsqdtr74hyu0\r\n$-1\r\n"
);
assert_eq!(f.run(&[b"GEOPOS", b"nokey", b"a"]), "*1\r\n*-1\r\n");
assert_eq!(f.run(&[b"GEOHASH", b"nokey", b"a"]), "*1\r\n$-1\r\n");
}
#[test]
fn a_distance_comes_back_with_four_places_in_whatever_unit_was_asked_for() {
let mut f = Fixture::new();
sicily(&mut f);
assert_eq!(
f.run(&[b"GEODIST", b"Sicily", b"Palermo", b"Catania"]),
"$11\r\n166274.1516\r\n"
);
assert_eq!(
f.run(&[b"GEODIST", b"Sicily", b"Palermo", b"Catania", b"km"]),
"$8\r\n166.2742\r\n"
);
assert_eq!(
f.run(&[b"GEODIST", b"Sicily", b"Palermo", b"Catania", b"mi"]),
"$8\r\n103.3182\r\n"
);
assert_eq!(
f.run(&[b"GEODIST", b"Sicily", b"Palermo", b"Foo"]),
"$-1\r\n"
);
assert_eq!(f.run(&[b"GEODIST", b"nokey", b"a", b"b"]), "$-1\r\n");
assert_eq!(
f.run(&[b"GEODIST", b"nokey", b"a", b"b", b"parsecs"]),
"-ERR unsupported unit provided. please use M, KM, FT, MI\r\n"
);
assert_eq!(
f.run(&[b"GEODIST", b"Sicily", b"a", b"b", b"km", b"extra"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_search_finds_what_is_inside_it_nearest_first() {
let mut f = Fixture::new();
sicily(&mut f);
let all = "*3\r\n$7\r\nCatania\r\n$9\r\nAgrigento\r\n$7\r\nPalermo\r\n";
assert_eq!(
f.run(&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"200",
b"km",
b"ASC"
]),
all
);
assert_eq!(
f.run(&[b"GEORADIUS", b"Sicily", b"15", b"37", b"200", b"km", b"ASC"]),
all
);
assert_eq!(
f.run(&[
b"GEORADIUS_RO",
b"Sicily",
b"15",
b"37",
b"200",
b"km",
b"ASC"
]),
all
);
assert_eq!(
f.run(&[
b"GEORADIUS",
b"Sicily",
b"15",
b"37",
b"200",
b"km",
b"DESC",
b"COUNT",
b"1"
]),
"*1\r\n$7\r\nPalermo\r\n"
);
assert_eq!(
f.run(&[
b"GEORADIUS",
b"Sicily",
b"15",
b"37",
b"200",
b"km",
b"COUNT",
b"1"
]),
"*1\r\n$7\r\nCatania\r\n"
);
let empty = "*0\r\n";
assert_eq!(
f.run(&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"1",
b"km"
]),
empty
);
assert_eq!(
f.run(&[
b"GEOSEARCH",
b"nokey",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"1",
b"km"
]),
empty
);
assert_eq!(
f.run(&[b"GEORADIUSBYMEMBER", b"nokey", b"m", b"1", b"km"]),
empty
);
}
#[test]
fn a_search_centred_on_a_member_starts_from_where_that_member_is() {
let mut f = Fixture::new();
sicily(&mut f);
assert_eq!(
f.run(&[b"GEORADIUSBYMEMBER", b"Sicily", b"Agrigento", b"100", b"km"]),
"*2\r\n$9\r\nAgrigento\r\n$7\r\nPalermo\r\n"
);
let with_dist = "*2\r\n*2\r\n$9\r\nAgrigento\r\n$6\r\n0.0000\r\n*2\r\n$7\r\nPalermo\r\n$7\r\n90.9778\r\n";
assert_eq!(
f.run(&[
b"GEORADIUSBYMEMBER_RO",
b"Sicily",
b"Agrigento",
b"100",
b"km",
b"WITHDIST"
]),
with_dist
);
assert_eq!(
f.run(&[
b"GEOSEARCH",
b"Sicily",
b"FROMMEMBER",
b"Agrigento",
b"BYRADIUS",
b"100",
b"km",
b"ASC",
b"WITHDIST"
]),
with_dist
);
assert_eq!(
f.run(&[b"GEORADIUSBYMEMBER", b"Sicily", b"Nowhere", b"100", b"km"]),
"-ERR could not decode requested zset member\r\n"
);
}
#[test]
fn a_box_search_reports_the_distance_the_hash_and_the_coordinates() {
let mut f = Fixture::new();
sicily(&mut f);
assert_eq!(
f.run(&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYBOX",
b"400",
b"400",
b"km",
b"ASC",
b"WITHCOORD",
b"WITHDIST",
b"WITHHASH"
]),
"*3\r\n*4\r\n$7\r\nCatania\r\n$7\r\n56.4413\r\n:3479447370796909\r\n*2\r\n\
$18\r\n15.087267458438873\r\n$17\r\n37.50266842333162\r\n\
*4\r\n$9\r\nAgrigento\r\n$8\r\n130.4235\r\n:3479030013248308\r\n*2\r\n\
$18\r\n13.583331406116486\r\n$18\r\n37.316668049938166\r\n\
*4\r\n$7\r\nPalermo\r\n$8\r\n190.4424\r\n:3479099956230698\r\n*2\r\n\
$18\r\n13.361389338970184\r\n$16\r\n38.1155563954963\r\n"
);
}
#[test]
fn a_store_writes_the_hashes_and_a_storedist_writes_the_distances() {
let mut f = Fixture::new();
sicily(&mut f);
let hashes = "*6\r\n$9\r\nAgrigento\r\n$16\r\n3479030013248308\r\n\
$7\r\nPalermo\r\n$16\r\n3479099956230698\r\n\
$7\r\nCatania\r\n$16\r\n3479447370796909\r\n";
assert_eq!(
f.run(&[
b"GEOSEARCHSTORE",
b"dst",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"200",
b"km",
b"ASC"
]),
":3\r\n"
);
assert_eq!(
f.run(&[b"ZRANGE", b"dst", b"0", b"-1", b"WITHSCORES"]),
hashes
);
assert_eq!(
f.run(&[
b"GEORADIUS",
b"Sicily",
b"15",
b"37",
b"200",
b"km",
b"STORE",
b"dst3"
]),
":3\r\n"
);
assert_eq!(
f.run(&[b"ZRANGE", b"dst3", b"0", b"-1", b"WITHSCORES"]),
hashes
);
assert_eq!(
f.run(&[
b"GEOSEARCHSTORE",
b"dst2",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"200",
b"km",
b"ASC",
b"STOREDIST"
]),
":3\r\n"
);
for (member, want) in [
("Catania", 56.441_257_870_158_19),
("Agrigento", 130.423_487_067_147_14),
("Palermo", 190.442_429_847_757_92),
] {
let reply = f.run(&[b"ZSCORE", b"dst2", member.as_bytes()]);
let got: f64 = reply
.trim_start_matches(|c: char| c != '\n')
.trim()
.parse()
.unwrap_or_else(|_| panic!("{member} scored {reply:?}"));
assert!(
(got - want).abs() < 1e-9,
"{member} scored {got} not {want}"
);
}
assert_eq!(
f.run(&[b"ZRANGE", b"dst2", b"0", b"-1"]),
"*3\r\n$7\r\nCatania\r\n$9\r\nAgrigento\r\n$7\r\nPalermo\r\n"
);
assert_eq!(
f.run(&[
b"GEOSEARCHSTORE",
b"dst",
b"nokey",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"200",
b"km"
]),
":0\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"dst"]), ":0\r\n");
}
#[test]
fn the_gates_on_geoadd_are_the_ones_zadd_has() {
let mut f = Fixture::new();
sicily(&mut f);
assert_eq!(
f.run(&[
b"GEOADD",
b"Sicily",
b"XX",
b"CH",
b"13.361389",
b"38.115556",
b"Palermo"
]),
":0\r\n"
);
assert_eq!(
f.run(&[
b"GEOADD",
b"Sicily",
b"NX",
b"13.361389",
b"38.9",
b"Palermo"
]),
":0\r\n"
);
assert_eq!(
f.run(&[
b"GEOADD",
b"Sicily",
b"CH",
b"13.361389",
b"38.9",
b"Palermo"
]),
":1\r\n"
);
assert_eq!(
f.run(&[
b"GEOADD",
b"new",
b"13.361389",
b"38.115556",
b"here",
b"181",
b"38",
b"there"
]),
"-ERR invalid longitude,latitude pair 181.000000,38.000000\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"new"]), ":0\r\n");
assert_eq!(
f.run(&[b"GEOADD", b"new", b"x", b"38", b"here"]),
"-ERR value is not a valid float\r\n"
);
assert_eq!(
f.run(&[b"GEOADD", b"new", b"13", b"38", b"here", b"and"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"GEOADD", b"new", b"NX", b"XX", b"CH"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"GEOADD", b"new", b"CH", b"CH", b"CH", b"CH"]),
"-ERR syntax error\r\n"
);
assert_eq!(
f.run(&[b"GEOADD", b"new", b"NX", b"CH"]),
"-ERR wrong number of arguments for 'geoadd' command\r\n"
);
}
#[test]
fn every_way_a_search_can_be_written_wrong_has_its_own_sentence() {
let mut f = Fixture::new();
sicily(&mut f);
let cases: &[(&[&[u8]], &str)] = &[
(
&[b"GEORADIUS", b"Sicily", b"15", b"37", b"x", b"km"],
"-ERR need numeric radius\r\n",
),
(
&[b"GEORADIUS", b"Sicily", b"15", b"37", b"-1", b"km"],
"-ERR radius cannot be negative\r\n",
),
(
&[b"GEORADIUS", b"Sicily", b"15", b"37", b"1", b"parsecs"],
"-ERR unsupported unit provided. please use M, KM, FT, MI\r\n",
),
(
&[b"GEORADIUS", b"Sicily", b"181", b"37", b"1", b"km"],
"-ERR invalid longitude,latitude pair 181.000000,37.000000\r\n",
),
(
&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYBOX",
b"x",
b"1",
b"km",
],
"-ERR need numeric width\r\n",
),
(
&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYBOX",
b"1",
b"y",
b"km",
],
"-ERR need numeric height\r\n",
),
(
&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYBOX",
b"-1",
b"1",
b"km",
],
"-ERR height or width cannot be negative\r\n",
),
(
&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"1",
b"km",
b"ANY",
],
"-ERR the ANY argument requires COUNT argument\r\n",
),
(
&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"1",
b"km",
b"COUNT",
b"0",
],
"-ERR COUNT must be > 0\r\n",
),
(
&[
b"GEOSEARCH",
b"Sicily",
b"BYRADIUS",
b"1",
b"km",
b"BYBOX",
b"1",
b"1",
b"km",
],
"-ERR syntax error\r\n",
),
(
&[
b"GEOSEARCH",
b"Sicily",
b"FROMMEMBER",
b"Palermo",
b"FROMLONLAT",
b"1",
b"2",
b"BYRADIUS",
b"1",
b"km",
],
"-ERR syntax error\r\n",
),
(
&[
b"geosearch",
b"Sicily",
b"BYRADIUS",
b"1",
b"km",
b"ASC",
b"WITHDIST",
],
"-ERR exactly one of FROMMEMBER or FROMLONLAT can be specified for geosearch\r\n",
),
(
&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"ASC",
b"WITHDIST",
],
"-ERR exactly one of BYRADIUS and BYBOX can be specified for GEOSEARCH\r\n",
),
(
&[
b"GEOSEARCHSTORE",
b"d",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"1",
b"km",
b"WITHCOORD",
],
"-ERR GEOSEARCHSTORE is not compatible with WITHDIST, WITHHASH and WITHCOORD options\r\n",
),
(
&[
b"GEORADIUS",
b"Sicily",
b"15",
b"37",
b"1",
b"km",
b"WITHDIST",
b"STORE",
b"d",
],
"-ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORD options\r\n",
),
(
&[
b"GEORADIUS_RO",
b"Sicily",
b"15",
b"37",
b"1",
b"km",
b"STORE",
b"d",
],
"-ERR syntax error\r\n",
),
(
&[
b"GEOSEARCH",
b"Sicily",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"1",
b"km",
b"STOREDIST",
],
"-ERR syntax error\r\n",
),
];
for (parts, want) in cases {
assert_eq!(&f.run(parts), want, "{:?}", parts[0]);
}
}
#[test]
fn every_geo_command_says_wrongtype() {
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 cases: &[&[&[u8]]] = &[
&[b"GEOADD", b"s", b"13", b"38", b"m"],
&[b"GEOPOS", b"s", b"m"],
&[b"GEOHASH", b"s", b"m"],
&[b"GEODIST", b"s", b"a", b"b"],
&[
b"GEOSEARCH",
b"s",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"1",
b"km",
],
&[
b"GEOSEARCHSTORE",
b"d",
b"s",
b"FROMLONLAT",
b"15",
b"37",
b"BYRADIUS",
b"1",
b"km",
],
&[b"GEORADIUS", b"s", b"15", b"37", b"1", b"km"],
&[b"GEORADIUS_RO", b"s", b"15", b"37", b"1", b"km"],
&[b"GEORADIUSBYMEMBER", b"s", b"m", b"1", b"km"],
&[b"GEORADIUSBYMEMBER_RO", b"s", b"m", b"1", b"km"],
];
for case in cases {
assert_eq!(f.run(case), wrong, "{:?}", case[0]);
}
assert_eq!(
f.run(&[b"GEORADIUS", b"s", b"15", b"37", b"x", b"km"]),
wrong
);
}
#[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"ARGREP".as_ref(), b"s", b"0", b"1", b"EXACT", b"v"][..],
&[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);
assert_eq!(f.run(&[b"ARGREP", b"s", b"-1", b"0", b"EXACT", b"v"]), 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 a_grep_answers_the_indexes_whose_elements_match() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"ARGREP", b"nope", b"0", b"10", b"EXACT", b"x"]),
"*0\r\n"
);
f.run(&[b"ARSET", b"a", b"0", b"alpha", b"beta", b"gamma", b"ALPHA"]);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"-", b"+", b"GLOB", b"*a"]),
"*3\r\n:0\r\n:1\r\n:2\r\n"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"+", b"-", b"GLOB", b"*a"]),
"*3\r\n:2\r\n:1\r\n:0\r\n"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"1", b"2", b"GLOB", b"*a"]),
"*2\r\n:1\r\n:2\r\n"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"-", b"+", b"EXACT", b"alpha"]),
"*1\r\n:0\r\n"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"-", b"+", b"EXACT", b"alpha", b"NOCASE"]),
"*2\r\n:0\r\n:3\r\n"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"-", b"+", b"MATCH", b"mm"]),
"*1\r\n:2\r\n"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"-", b"+", b"RE", b"^[bg]"]),
"*2\r\n:1\r\n:2\r\n"
);
let both: &[&[u8]] = &[
b"ARGREP", b"a", b"-", b"+", b"EXACT", b"beta", b"MATCH", b"al",
];
assert_eq!(f.run(both), "*2\r\n:0\r\n:1\r\n");
assert_eq!(
f.run(&[
b"ARGREP", b"a", b"-", b"+", b"EXACT", b"beta", b"MATCH", b"al", b"AND"
]),
"*0\r\n"
);
assert_eq!(
f.run(&[
b"ARGREP", b"a", b"-", b"+", b"EXACT", b"beta", b"MATCH", b"al", b"AND", b"OR"
]),
"*2\r\n:0\r\n:1\r\n"
);
assert_eq!(
f.run(&[
b"ARGREP",
b"a",
b"-",
b"+",
b"MATCH",
b"a",
b"WITHVALUES",
b"LIMIT",
b"2"
]),
"*2\r\n*2\r\n:0\r\n$5\r\nalpha\r\n*2\r\n:1\r\n$4\r\nbeta\r\n"
);
assert_eq!(
f.run(&[
b"ARGREP", b"a", b"-", b"+", b"EXACT", b"ALPHA", b"LIMIT", b"1"
]),
"*1\r\n:3\r\n"
);
}
#[test]
fn a_grep_reports_a_broken_command_the_way_redis_does() {
let mut f = Fixture::new();
f.run(&[b"ARSET", b"a", b"0", b"alpha"]);
let syntax = "-ERR syntax error\r\n";
assert_eq!(
f.run(&[b"ARGREP", b"a", b"-1", b"0", b"NOPE", b"x"]),
"-ERR invalid array index\r\n"
);
assert_eq!(f.run(&[b"ARGREP", b"a", b"0", b"1", b"NOPE", b"x"]), syntax);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"0", b"1", b"NOCASE", b"EXACT"]),
syntax
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"0", b"1", b"EXACT", b"x", b"LIMIT"]),
syntax
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"0", b"1", b"NOCASE", b"WITHVALUES"]),
syntax,
"a command with no predicate in it at all"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"0", b"1", b"EXACT", b"x", b"LIMIT", b"0"]),
"-ERR LIMIT must be positive\r\n"
);
assert_eq!(
f.run(&[
b"ARGREP", b"a", b"0", b"1", b"EXACT", b"x", b"LIMIT", b"nine"
]),
"-ERR value is not an integer or out of range\r\n"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"0", b"1", b"RE", b""]),
"-ERR regular expression is empty\r\n"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"0", b"1", b"RE", b"(a"]),
"-ERR invalid regular expression: Missing ')'\r\n"
);
assert_eq!(
f.run(&[b"ARGREP", b"a", b"0", b"1", b"RE", br"(a)\1"]),
"-ERR regular expression backreferences are not supported\r\n"
);
let arity = "-ERR wrong number of arguments for 'argrep' command\r\n";
assert_eq!(f.run(&[b"ARGREP", b"a", b"0", b"1", b"EXACT"]), arity);
assert_eq!(f.run(&[b"ARGREP", b"a", b"0", b"1"]), arity);
}
#[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"
);
}
#[test]
fn a_node_comes_back_with_the_fields_it_went_in_with() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[
b"G.NADD", b"social", b"ada", b"name", b"Ada", b"born", b"1815"
]),
":1\r\n"
);
assert_eq!(
f.run(&[b"G.NGET", b"social", b"ada"]),
"*4\r\n$4\r\nborn\r\n$4\r\n1815\r\n$4\r\nname\r\n$3\r\nAda\r\n"
);
assert_eq!(
f.run(&[b"G.NADD", b"social", b"ada", b"name", b"Ada Lovelace"]),
":0\r\n"
);
assert_eq!(
f.run(&[b"G.NGET", b"social", b"ada"]),
"*2\r\n$4\r\nname\r\n$12\r\nAda Lovelace\r\n"
);
assert_eq!(f.run(&[b"G.NADD", b"social", b"grace"]), ":1\r\n");
assert_eq!(f.run(&[b"G.NGET", b"social", b"grace"]), "*0\r\n");
assert_eq!(f.run(&[b"G.NGET", b"social", b"nobody"]), "$-1\r\n");
assert_eq!(f.run(&[b"G.NGET", b"nokey", b"ada"]), "$-1\r\n");
assert_eq!(
f.run(&[b"G.NADD", b"fresh", b"n", b"lonely"]),
"-ERR syntax error\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"fresh"]), ":0\r\n");
let mut g = Fixture::new();
g.run(&[b"HELLO", b"3"]);
g.run(&[b"G.NADD", b"social", b"ada", b"name", b"Ada"]);
assert_eq!(
g.run(&[b"G.NGET", b"social", b"ada"]),
"%1\r\n$4\r\nname\r\n$3\r\nAda\r\n"
);
}
#[test]
fn an_edge_creates_the_ends_it_needs() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[
b"G.EADD", b"social", b"ada", b"grace", b"FOLLOWS", b"since", b"1843"
]),
":1\r\n"
);
assert_eq!(f.run(&[b"G.NGET", b"social", b"ada"]), "*0\r\n");
assert_eq!(f.run(&[b"G.NGET", b"social", b"grace"]), "*0\r\n");
assert_eq!(
f.run(&[b"G.OUT", b"social", b"ada", b"FOLLOWS"]),
"*2\r\n$1\r\n0\r\n*1\r\n$5\r\ngrace\r\n"
);
assert_eq!(
f.run(&[b"G.IN", b"social", b"grace", b"FOLLOWS"]),
"*2\r\n$1\r\n0\r\n*1\r\n$3\r\nada\r\n"
);
assert_eq!(
f.run(&[
b"G.EADD", b"social", b"ada", b"grace", b"FOLLOWS", b"since", b"1844"
]),
":0\r\n"
);
assert_eq!(f.run(&[b"G.DEG", b"social", b"ada", b"FOLLOWS"]), ":1\r\n");
assert_eq!(
f.run(&[b"G.EADD", b"social", b"ada", b"grace", b"WORKS_WITH"]),
":1\r\n"
);
assert_eq!(
f.run(&[b"G.DEG", b"social", b"ada", b"WORKS_WITH"]),
":1\r\n"
);
assert_eq!(
f.run(&[b"G.EDEL", b"social", b"ada", b"grace", b"FOLLOWS"]),
":1\r\n"
);
assert_eq!(
f.run(&[b"G.EDEL", b"social", b"ada", b"grace", b"FOLLOWS"]),
":0\r\n"
);
assert_eq!(
f.run(&[b"G.EDEL", b"social", b"ada", b"grace", b"NEVER"]),
":0\r\n"
);
assert_eq!(
f.run(&[b"G.EDEL", b"social", b"ada", b"nobody", b"FOLLOWS"]),
":0\r\n"
);
assert_eq!(
f.run(&[b"G.EDEL", b"nokey", b"ada", b"grace", b"FOLLOWS"]),
":0\r\n"
);
}
#[test]
fn a_hop_answers_a_cursor_and_a_page() {
let mut f = Fixture::new();
for i in 0..25u32 {
let dst = format!("n{i}");
f.run(&[b"G.EADD", b"social", b"hub", dst.as_bytes(), b"FOLLOWS"]);
}
let first = f.run(&[b"G.OUT", b"social", b"hub", b"FOLLOWS"]);
assert!(first.starts_with("*2\r\n$2\r\n10\r\n*10\r\n"), "{first}");
let mut seen = 0;
let mut cursor = String::from("0");
loop {
let page = f.run(&[
b"G.OUT",
b"social",
b"hub",
b"FOLLOWS",
b"COUNT",
b"7",
b"CURSOR",
cursor.as_bytes(),
]);
let (head, rest) = page.split_once("\r\n*").expect("a cursor and a page");
cursor = head
.rsplit("\r\n")
.next()
.expect("the cursor line")
.to_string();
seen += rest
.split_once("\r\n")
.expect("the page length")
.0
.parse::<usize>()
.expect("a length");
if cursor == "0" {
break;
}
}
assert_eq!(seen, 25, "every neighbour once across the pages");
assert_eq!(
f.run(&[b"G.OUT", b"social", b"hub", b"FOLLOWS", b"CURSOR", b"900"]),
"*2\r\n$1\r\n0\r\n*0\r\n"
);
assert_eq!(
f.run(&[b"G.OUT", b"social", b"hub", b"NEVER"]),
"*2\r\n$1\r\n0\r\n*0\r\n"
);
assert_eq!(
f.run(&[b"G.OUT", b"nokey", b"hub", b"FOLLOWS"]),
"*2\r\n$1\r\n0\r\n*0\r\n"
);
assert_eq!(
f.run(&[b"G.OUT", b"social", b"hub", b"FOLLOWS", b"COUNT", b"0"]),
"-ERR COUNT must be a positive integer\r\n"
);
assert_eq!(
f.run(&[b"G.OUT", b"social", b"hub", b"FOLLOWS", b"NOPE", b"1"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_degree_counts_one_way_or_both() {
let mut f = Fixture::new();
f.run(&[b"G.EADD", b"social", b"a", b"b", b"F"]);
f.run(&[b"G.EADD", b"social", b"a", b"c", b"F"]);
f.run(&[b"G.EADD", b"social", b"d", b"a", b"F"]);
assert_eq!(f.run(&[b"G.DEG", b"social", b"a", b"F"]), ":2\r\n");
assert_eq!(f.run(&[b"G.DEG", b"social", b"a", b"F", b"OUT"]), ":2\r\n");
assert_eq!(f.run(&[b"G.DEG", b"social", b"a", b"F", b"IN"]), ":1\r\n");
assert_eq!(f.run(&[b"G.DEG", b"social", b"a", b"F", b"BOTH"]), ":3\r\n");
assert_eq!(f.run(&[b"G.DEG", b"social", b"nobody", b"F"]), ":0\r\n");
assert_eq!(f.run(&[b"G.DEG", b"social", b"a", b"NEVER"]), ":0\r\n");
assert_eq!(f.run(&[b"G.DEG", b"nokey", b"a", b"F"]), ":0\r\n");
assert_eq!(
f.run(&[b"G.DEG", b"social", b"a", b"F", b"SIDEWAYS"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_walk_reaches_each_node_once_however_many_ways_there_are() {
let mut f = Fixture::new();
for (src, dst) in [
("ada", "grace"),
("ada", "alan"),
("grace", "edsger"),
("alan", "edsger"),
("edsger", "barbara"),
] {
f.run(&[b"G.EADD", b"social", src.as_bytes(), dst.as_bytes(), b"F"]);
}
assert_eq!(
f.run(&[b"G.NEIGH", b"social", b"ada", b"F"]),
"*3\r\n$5\r\ngrace\r\n$4\r\nalan\r\n$6\r\nedsger\r\n"
);
assert_eq!(
f.run(&[b"G.NEIGH", b"social", b"ada", b"F", b"DEPTH", b"1"]),
"*2\r\n$5\r\ngrace\r\n$4\r\nalan\r\n"
);
let deep = f.run(&[b"G.NEIGH", b"social", b"ada", b"F", b"DEPTH", b"9"]);
assert!(deep.starts_with("*4\r\n"), "the whole component: {deep}");
assert!(deep.contains("$7\r\nbarbara\r\n"), "{deep}");
assert_eq!(
f.run(&[b"G.NEIGH", b"social", b"ada", b"F", b"COUNT", b"1"]),
"*1\r\n$5\r\ngrace\r\n"
);
assert_eq!(f.run(&[b"G.NEIGH", b"social", b"barbara", b"F"]), "*0\r\n");
assert_eq!(f.run(&[b"G.NEIGH", b"social", b"ada", b"NEVER"]), "*0\r\n");
assert_eq!(f.run(&[b"G.NEIGH", b"nokey", b"ada", b"F"]), "*0\r\n");
assert_eq!(
f.run(&[b"G.NEIGH", b"social", b"ada", b"F", b"DEPTH", b"0"]),
"-ERR DEPTH must be a positive integer\r\n"
);
assert_eq!(
f.run(&[b"G.NEIGH", b"social", b"ada", b"F", b"NOPE", b"1"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn a_path_is_the_shortest_one_and_goes_over_any_label() {
let mut f = Fixture::new();
for i in 0..6u32 {
let src = format!("n{i}");
let dst = format!("n{}", i + 1);
f.run(&[b"G.EADD", b"road", src.as_bytes(), dst.as_bytes(), b"STEP"]);
}
assert_eq!(
f.run(&[b"G.PATH", b"road", b"n0", b"n6"]),
"*7\r\n$2\r\nn0\r\n$2\r\nn1\r\n$2\r\nn2\r\n$2\r\nn3\r\n$2\r\nn4\r\n$2\r\nn5\r\n$2\r\nn6\r\n"
);
f.run(&[b"G.EADD", b"road", b"n0", b"n5", b"JUMP"]);
assert_eq!(
f.run(&[b"G.PATH", b"road", b"n0", b"n6"]),
"*3\r\n$2\r\nn0\r\n$2\r\nn5\r\n$2\r\nn6\r\n"
);
assert_eq!(
f.run(&[b"G.PATH", b"road", b"n2", b"n2"]),
"*1\r\n$2\r\nn2\r\n"
);
assert_eq!(
f.run(&[b"G.PATH", b"road", b"n0", b"n6", b"MAXDEPTH", b"1"]),
"*0\r\n"
);
assert_eq!(f.run(&[b"G.PATH", b"road", b"n6", b"n0"]), "*0\r\n");
f.run(&[b"G.NADD", b"road", b"island"]);
assert_eq!(f.run(&[b"G.PATH", b"road", b"n0", b"island"]), "*0\r\n");
assert_eq!(f.run(&[b"G.PATH", b"road", b"n0", b"nobody"]), "*0\r\n");
assert_eq!(f.run(&[b"G.PATH", b"nokey", b"n0", b"n6"]), "*0\r\n");
assert_eq!(
f.run(&[b"G.PATH", b"road", b"n0", b"n6", b"NOPE", b"3"]),
"-ERR syntax error\r\n"
);
}
#[test]
fn the_keyspace_sees_a_graph_key_like_any_other() {
let mut f = Fixture::new();
f.run(&[b"G.EADD", b"social", b"ada", b"grace", b"F"]);
assert_eq!(f.run(&[b"TYPE", b"social"]), "+graph\r\n");
assert_eq!(
f.run(&[b"OBJECT", b"ENCODING", b"social"]),
"$9\r\nadjacency\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"social"]), ":1\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":1\r\n");
assert_eq!(f.run(&[b"KEYS", b"*"]), "*1\r\n$6\r\nsocial\r\n");
let held = f.server.memory_bytes();
for i in 0..200u32 {
let dst = format!("n{i}");
f.run(&[b"G.EADD", b"big", b"hub", dst.as_bytes(), b"F"]);
}
assert!(
f.server.memory_bytes() > held,
"two hundred edges cost something: {held} then {}",
f.server.memory_bytes()
);
f.run(&[b"DEL", b"big"]);
assert_eq!(f.run(&[b"EXPIRE", b"social", b"100"]), ":1\r\n");
assert_eq!(f.run(&[b"PERSIST", b"social"]), ":1\r\n");
assert_eq!(f.run(&[b"RENAME", b"social", b"net"]), "+OK\r\n");
assert_eq!(f.run(&[b"MOVE", b"net", b"1"]), ":1\r\n");
assert_eq!(f.run(&[b"EXISTS", b"net"]), ":0\r\n");
f.run(&[b"SELECT", b"1"]);
assert_eq!(f.run(&[b"G.DEG", b"net", b"ada", b"F"]), ":1\r\n");
assert_eq!(f.run(&[b"DEL", b"net"]), ":1\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
f.run(&[b"G.NADD", b"g", b"n"]);
assert_eq!(f.run(&[b"FLUSHDB"]), "+OK\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
}
#[test]
fn a_graph_cannot_be_copied_or_dumped() {
let mut f = Fixture::new();
f.run(&[b"G.NADD", b"social", b"ada"]);
assert_eq!(
f.run(&[b"COPY", b"social", b"other"]),
"-ERR COPY is not supported for a graph\r\n"
);
assert_eq!(
f.run(&[b"COPY", b"social", b"other", b"DB", b"1"]),
"-ERR COPY is not supported for a graph\r\n"
);
assert_eq!(
f.run(&[b"DUMP", b"social"]),
"-ERR DUMP is not supported for a graph\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"social", b"other"]), ":1\r\n");
}
#[test]
fn a_graph_and_a_string_are_the_wrong_type_for_each_other() {
let wrong = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
let mut f = Fixture::new();
f.run(&[b"G.NADD", b"social", b"ada"]);
assert_eq!(f.run(&[b"GET", b"social"]), wrong);
assert_eq!(f.run(&[b"LPUSH", b"social", b"x"]), wrong);
assert_eq!(f.run(&[b"SADD", b"social", b"x"]), wrong);
f.run(&[b"SET", b"str", b"v"]);
for cmd in [
vec![b"G.NADD".as_ref(), b"str", b"n"],
vec![b"G.NGET".as_ref(), b"str", b"n"],
vec![b"G.NDEL".as_ref(), b"str", b"n"],
vec![b"G.EADD".as_ref(), b"str", b"a", b"b", b"F"],
vec![b"G.EDEL".as_ref(), b"str", b"a", b"b", b"F"],
vec![b"G.OUT".as_ref(), b"str", b"a", b"F"],
vec![b"G.IN".as_ref(), b"str", b"a", b"F"],
vec![b"G.DEG".as_ref(), b"str", b"a", b"F"],
vec![b"G.NEIGH".as_ref(), b"str", b"a", b"F"],
vec![b"G.PATH".as_ref(), b"str", b"a", b"b"],
] {
assert_eq!(f.run(&cmd), wrong, "{:?}", cmd[0]);
}
}
#[test]
fn a_graph_goes_when_its_last_node_does() {
let mut f = Fixture::new();
f.run(&[
b"G.EADD", b"social", b"ada", b"grace", b"F", b"since", b"1843",
]);
assert_eq!(f.run(&[b"G.NDEL", b"social", b"ada"]), ":1\r\n");
assert_eq!(f.run(&[b"G.NGET", b"social", b"ada"]), "$-1\r\n");
assert_eq!(
f.run(&[b"G.DEG", b"social", b"grace", b"F", b"IN"]),
":0\r\n"
);
assert_eq!(f.run(&[b"G.NDEL", b"social", b"ada"]), ":0\r\n");
assert_eq!(f.run(&[b"EXISTS", b"social"]), ":1\r\n");
assert_eq!(f.run(&[b"G.NDEL", b"social", b"grace"]), ":1\r\n");
assert_eq!(f.run(&[b"EXISTS", b"social"]), ":0\r\n");
assert_eq!(f.run(&[b"DBSIZE"]), ":0\r\n");
assert_eq!(f.run(&[b"G.NDEL", b"nokey", b"ada"]), ":0\r\n");
f.run(&[b"G.NADD", b"social", b"first"]);
f.run(&[b"G.NADD", b"social", b"second"]);
f.run(&[b"G.NDEL", b"social", b"first"]);
f.run(&[b"G.EADD", b"social", b"third", b"second", b"F"]);
assert_eq!(
f.run(&[b"G.OUT", b"social", b"third", b"F"]),
"*2\r\n$1\r\n0\r\n*1\r\n$6\r\nsecond\r\n"
);
}
#[test]
fn xadd_ids_only_ever_go_up() {
let mut f = Fixture::new();
assert_eq!(f.run(&[b"XADD", b"s", b"5", b"a", b"1"]), "$3\r\n5-0\r\n");
assert_eq!(f.run(&[b"XADD", b"s", b"5-*", b"a", b"2"]), "$3\r\n5-1\r\n");
assert_eq!(f.run(&[b"XADD", b"s", b"5-*", b"a", b"3"]), "$3\r\n5-2\r\n");
assert_eq!(f.run(&[b"XADD", b"s", b"6-9", b"a", b"4"]), "$3\r\n6-9\r\n");
assert_eq!(f.run(&[b"XLEN", b"s"]), ":4\r\n");
assert!(
f.run(&[b"XADD", b"s", b"6-9", b"a", b"5"])
.contains("equal or smaller")
);
assert!(
f.run(&[b"XADD", b"s", b"0-0", b"a", b"5"])
.contains("must be greater than 0-0")
);
assert!(
f.run(&[b"XADD", b"s", b"nonsense", b"a", b"5"])
.contains("Invalid stream ID")
);
assert!(
f.run(&[b"XADD", b"s", b"*", b"a"])
.contains("wrong number of arguments")
);
assert_eq!(
f.run(&[b"XADD", b"gone", b"NOMKSTREAM", b"*", b"a", b"1"]),
"$-1\r\n"
);
assert_eq!(f.run(&[b"EXISTS", b"gone"]), ":0\r\n");
assert_eq!(f.run(&[b"TYPE", b"s"]), "+stream\r\n");
assert_eq!(f.run(&[b"OBJECT", b"ENCODING", b"s"]), "$6\r\nstream\r\n");
}
#[test]
fn trimming_reads_its_options_the_way_redis_does() {
let mut f = Fixture::new();
for i in 1..=10u32 {
f.run(&[b"XADD", b"s", format!("{i}-1").as_bytes(), b"a", b"1"]);
}
assert_eq!(f.run(&[b"XTRIM", b"s", b"MAXLEN", b"4"]), ":6\r\n");
assert_eq!(f.run(&[b"XLEN", b"s"]), ":4\r\n");
assert_eq!(f.run(&[b"XTRIM", b"s", b"MINID", b"9"]), ":2\r\n");
assert_eq!(f.run(&[b"XLEN", b"s"]), ":2\r\n");
assert!(
f.run(&[b"XTRIM", b"s", b"MAXLEN", b"~"])
.contains("not an integer")
);
assert!(
f.run(&[b"XTRIM", b"s", b"MAXLEN", b"-1"])
.contains("MAXLEN argument must be >= 0")
);
assert!(
f.run(&[b"XTRIM", b"s", b"LIMIT", b"5"])
.contains("without specifying a trimming strategy")
);
assert!(
f.run(&[b"XTRIM", b"s", b"MAXLEN", b"5", b"LIMIT", b"5"])
.contains("without the special ~ option")
);
assert!(
f.run(&[b"XTRIM", b"s", b"MAXLEN", b"5", b"MINID", b"5"])
.contains("at the same time are not compatible")
);
assert!(
f.run(&[b"XTRIM", b"s", b"NOMKSTREAM", b"MAXLEN", b"5"])
.contains("syntax error")
);
assert_eq!(f.run(&[b"XTRIM", b"missing", b"MAXLEN", b"5"]), ":0\r\n");
}
#[test]
fn xrange_looks_the_key_up_before_it_reads_the_count() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"5-1", b"a", b"1"]);
f.run(&[b"XADD", b"s", b"6-1", b"b", b"2"]);
assert_eq!(
f.run(&[b"XRANGE", b"s", b"-", b"+"]),
"*2\r\n*2\r\n$3\r\n5-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n\
*2\r\n$3\r\n6-1\r\n*2\r\n$1\r\nb\r\n$1\r\n2\r\n"
);
assert_eq!(
f.run(&[b"XREVRANGE", b"s", b"+", b"-", b"COUNT", b"1"]),
"*1\r\n*2\r\n$3\r\n6-1\r\n*2\r\n$1\r\nb\r\n$1\r\n2\r\n"
);
assert_eq!(
f.run(&[b"XRANGE", b"s", b"-", b"(6"]),
"*2\r\n*2\r\n$3\r\n5-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n\
*2\r\n$3\r\n6-1\r\n*2\r\n$1\r\nb\r\n$1\r\n2\r\n"
);
assert_eq!(
f.run(&[b"XRANGE", b"s", b"(5-1", b"+"]),
"*1\r\n*2\r\n$3\r\n6-1\r\n*2\r\n$1\r\nb\r\n$1\r\n2\r\n"
);
assert!(
f.run(&[b"XRANGE", b"s", b"(-", b"+"])
.contains("Invalid stream ID")
);
assert_eq!(
f.run(&[b"XRANGE", b"missing", b"-", b"+", b"COUNT", b"0"]),
"*0\r\n"
);
assert_eq!(
f.run(&[b"XRANGE", b"s", b"-", b"+", b"COUNT", b"0"]),
"*-1\r\n"
);
f.run(&[b"SET", b"str", b"v"]);
assert!(
f.run(&[b"XRANGE", b"str", b"-", b"+", b"COUNT", b"0"])
.starts_with("-WRONGTYPE")
);
assert_eq!(
f.run(&[b"XRANGE", b"s", b"-", b"+", b"COUNT", b"2", b"COUNT", b"1"]),
"*1\r\n*2\r\n$3\r\n5-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
}
#[test]
fn a_bad_id_late_in_the_list_stops_the_whole_command() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
f.run(&[b"XADD", b"s", b"2-1", b"a", b"2"]);
assert!(
f.run(&[b"XDEL", b"s", b"1-1", b"nonsense"])
.contains("Invalid stream ID")
);
assert_eq!(f.run(&[b"XLEN", b"s"]), ":2\r\n");
assert_eq!(f.run(&[b"XDEL", b"s", b"1-1", b"9-9"]), ":1\r\n");
assert_eq!(f.run(&[b"XLEN", b"s"]), ":1\r\n");
assert_eq!(f.run(&[b"XDEL", b"missing", b"1-1"]), ":0\r\n");
assert_eq!(f.run(&[b"XACK", b"missing", b"g", b"1-1"]), ":0\r\n");
}
#[test]
fn xgroup_has_an_arity_per_subcommand() {
let mut f = Fixture::new();
assert!(
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"$"])
.contains("requires the key")
);
assert_eq!(
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"$", b"MKSTREAM"]),
"+OK\r\n"
);
assert!(
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"$"])
.starts_with("-BUSYGROUP")
);
assert_eq!(
f.run(&[b"XGROUP", b"CREATECONSUMER", b"s", b"g", b"c"]),
":1\r\n"
);
assert_eq!(
f.run(&[b"XGROUP", b"CREATECONSUMER", b"s", b"g", b"c"]),
":0\r\n"
);
assert_eq!(
f.run(&[b"XGROUP", b"DELCONSUMER", b"s", b"g", b"c"]),
":0\r\n"
);
let short = f.run(&[b"XGROUP", b"DESTROY", b"s"]);
assert!(
short.contains("wrong number of arguments for 'xgroup|destroy' command"),
"{short}"
);
let odd = f.run(&[b"XGROUP", b"SETID", b"s", b"g", b"0", b"ENTRIESREAD"]);
assert!(
odd.contains("unknown subcommand or wrong number of arguments for 'SETID'"),
"{odd}"
);
assert!(
f.run(&[b"XGROUP", b"NOSUCH", b"s"])
.contains("Try XGROUP HELP")
);
assert_eq!(f.run(&[b"XGROUP", b"SETID", b"s", b"g", b"0"]), "+OK\r\n");
assert!(
f.run(&[b"XGROUP", b"SETID", b"s", b"nogroup", b"0"])
.starts_with("-NOGROUP")
);
assert_eq!(f.run(&[b"XGROUP", b"DESTROY", b"s", b"g"]), ":1\r\n");
assert_eq!(f.run(&[b"XGROUP", b"DESTROY", b"s", b"g"]), ":0\r\n");
assert!(
f.run(&[b"XGROUP", b"DESTROY", b"missing", b"g"])
.contains("requires the key")
);
}
#[test]
fn xreadgroup_hands_out_and_xack_takes_back() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
f.run(&[b"XADD", b"s", b"2-1", b"a", b"2"]);
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"0"]);
let first = f.run(&[
b"XREADGROUP",
b"GROUP",
b"g",
b"c1",
b"COUNT",
b"1",
b"STREAMS",
b"s",
b">",
]);
assert_eq!(
first,
"*1\r\n*2\r\n$1\r\ns\r\n*1\r\n*2\r\n$3\r\n1-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
assert_eq!(
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c2", b"STREAMS", b"s", b"0"]),
"*1\r\n*2\r\n$1\r\ns\r\n*0\r\n"
);
assert_eq!(
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c1", b"STREAMS", b"s", b"0"]),
"*1\r\n*2\r\n$1\r\ns\r\n*1\r\n*2\r\n$3\r\n1-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g"]),
"*4\r\n:1\r\n$3\r\n1-1\r\n$3\r\n1-1\r\n*1\r\n*2\r\n$2\r\nc1\r\n$1\r\n1\r\n"
);
assert_eq!(f.run(&[b"XACK", b"s", b"g", b"1-1"]), ":1\r\n");
assert_eq!(f.run(&[b"XACK", b"s", b"g", b"1-1"]), ":0\r\n");
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g"]),
"*4\r\n:0\r\n$-1\r\n$-1\r\n*-1\r\n"
);
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c1", b"STREAMS", b"s", b">"]);
f.run(&[b"XDEL", b"s", b"2-1"]);
assert_eq!(
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c1", b"STREAMS", b"s", b"0"]),
"*1\r\n*2\r\n$1\r\ns\r\n*1\r\n*2\r\n$3\r\n2-1\r\n$-1\r\n"
);
assert!(
f.run(&[
b"XREADGROUP",
b"GROUP",
b"nope",
b"c",
b"STREAMS",
b"s",
b"+"
])
.starts_with("-NOGROUP")
);
assert!(
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c", b"STREAMS", b"s", b"$"])
.contains("meaningless in the context of XREADGROUP")
);
assert!(
f.run(&[b"XREAD", b"GROUP", b"g", b"c", b"STREAMS", b"s", b"0"])
.contains("only supported by XREADGROUP")
);
assert!(
f.run(&[
b"XREADGROUP",
b"GROUP",
b"g",
b"c",
b"STREAMS",
b"s",
b"a",
b"b"
])
.contains("Unbalanced 'xreadgroup' list of streams")
);
}
#[test]
fn xread_with_no_block_writes_the_null_itself() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
assert_eq!(
f.run(&[b"XREAD", b"STREAMS", b"s", b"0"]),
"*1\r\n*2\r\n$1\r\ns\r\n*1\r\n*2\r\n$3\r\n1-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
assert_eq!(f.run(&[b"XREAD", b"STREAMS", b"s", b"1-1"]), "*-1\r\n");
assert_eq!(f.run(&[b"XREAD", b"STREAMS", b"missing", b"0"]), "*-1\r\n");
f.run(&[b"XADD", b"other", b"1-1", b"b", b"2"]);
assert_eq!(
f.run(&[b"XREAD", b"STREAMS", b"s", b"other", b"1-1", b"0"]),
"*1\r\n*2\r\n$5\r\nother\r\n*1\r\n*2\r\n$3\r\n1-1\r\n*2\r\n$1\r\nb\r\n$1\r\n2\r\n"
);
assert_eq!(f.run(&[b"XREAD", b"STREAMS", b"s", b"$"]), "*-1\r\n");
assert_eq!(
f.run(&[b"XREAD", b"COUNT", b"5", b"STREAMS", b"s", b"+"]),
"*1\r\n*2\r\n$1\r\ns\r\n*1\r\n*2\r\n$3\r\n1-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
assert_eq!(
f.run(&[b"XREAD", b"COUNT", b"0", b"STREAMS", b"s", b"0"]),
"*1\r\n*2\r\n$1\r\ns\r\n*1\r\n*2\r\n$3\r\n1-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
assert!(
f.run(&[b"XREAD", b"BLOCK", b"0.5", b"STREAMS", b"s", b"$"])
.contains("not an integer")
);
assert!(
f.run(&[b"XREAD", b"BLOCK", b"-1", b"STREAMS", b"s", b"$"])
.contains("timeout is negative")
);
assert!(
f.run(&[b"XREAD", b"STREAMS", b"s", b"other", b"0"])
.contains("Unbalanced 'xread' list of streams")
);
}
#[test]
fn a_blocked_xread_wakes_on_the_next_entry() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
let (flow, reply) = f.flow(&[b"XREAD", b"BLOCK", b"0", b"STREAMS", b"s", b"$"]);
assert_eq!(flow, Flow::Block);
assert!(reply.is_empty());
let (flow, _) = f.flow(&[b"XREAD", b"BLOCK", b"0", b"STREAMS", b"s", b"$"]);
assert_eq!(flow, Flow::Block);
assert_eq!(f.server.waiters().len(), 2);
f.run(&[b"XADD", b"s", b"2-1", b"a", b"2"]);
let want = "*1\r\n*2\r\n$1\r\ns\r\n*1\r\n*2\r\n$3\r\n2-1\r\n*2\r\n$1\r\na\r\n$1\r\n2\r\n";
for at in 0..2 {
let mut out = Out::new(Proto::Resp2);
assert!(f.server.serve_waiter(at, 0, &mut out));
assert_eq!(core::str::from_utf8(out.as_slice()).expect("ascii"), want);
}
f.server.waiters_mut().forget(7);
let (flow, _) = f.flow(&[b"XREAD", b"BLOCK", b"50", b"STREAMS", b"s", b"$"]);
assert_eq!(flow, Flow::Block);
let mut out = Out::new(Proto::Resp2);
assert!(!f.server.serve_waiter(0, 0, &mut out));
assert!(out.as_slice().is_empty());
assert!(f.server.serve_waiter(0, u64::MAX, &mut out));
assert_eq!(
core::str::from_utf8(out.as_slice()).expect("ascii"),
"*-1\r\n"
);
}
#[test]
fn losing_a_group_while_blocked_is_the_ordinary_sentence() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"$"]);
let (flow, _) = f.flow(&[
b"XREADGROUP",
b"GROUP",
b"g",
b"c",
b"BLOCK",
b"0",
b"STREAMS",
b"s",
b">",
]);
assert_eq!(flow, Flow::Block);
f.run(&[b"XGROUP", b"DESTROY", b"s", b"g"]);
let mut out = Out::new(Proto::Resp2);
assert!(f.server.serve_waiter(0, 0, &mut out));
assert_eq!(
core::str::from_utf8(out.as_slice()).expect("ascii"),
"-NOGROUP No such key 's' or consumer group 'g' in XREADGROUP with GROUP option\r\n"
);
}
#[test]
fn xclaim_reads_ids_until_one_will_not_parse() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
f.run(&[b"XADD", b"s", b"2-1", b"a", b"2"]);
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"0"]);
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c1", b"STREAMS", b"s", b">"]);
assert!(
f.run(&[b"XCLAIM", b"s", b"g", b"c2", b"0", b"-"])
.contains("Unrecognized XCLAIM option '-'")
);
assert_eq!(
f.run(&[b"XCLAIM", b"s", b"g", b"c2", b"0", b"1-1", b"JUSTID"]),
"*1\r\n$3\r\n1-1\r\n"
);
f.run(&[b"XDEL", b"s", b"2-1"]);
assert_eq!(
f.run(&[b"XCLAIM", b"s", b"g", b"c2", b"0", b"2-1"]),
"*0\r\n"
);
assert!(
f.run(&[b"XPENDING", b"s", b"g"])
.starts_with("*4\r\n:1\r\n")
);
assert!(
f.run(&[b"XCLAIM", b"s", b"nope", b"c", b"0", b"1-1"])
.starts_with("-NOGROUP")
);
assert!(
f.run(&[b"XCLAIM", b"s", b"g", b"c", b"nan", b"1-1"])
.contains("Invalid min-idle-time argument for XCLAIM")
);
}
#[test]
fn xautoclaim_reports_what_it_dropped() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
f.run(&[b"XADD", b"s", b"2-1", b"a", b"2"]);
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"0"]);
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c1", b"STREAMS", b"s", b">"]);
f.run(&[b"XDEL", b"s", b"1-1"]);
assert_eq!(
f.run(&[b"XAUTOCLAIM", b"s", b"g", b"c2", b"0", b"-", b"JUSTID"]),
"*3\r\n$3\r\n0-0\r\n*1\r\n$3\r\n2-1\r\n*1\r\n$3\r\n1-1\r\n"
);
assert!(
f.run(&[b"XAUTOCLAIM", b"s", b"g", b"c2", b"0", b"-", b"COUNT", b"0"])
.contains("COUNT must be > 0")
);
assert!(
f.run(&[b"XAUTOCLAIM", b"s", b"nope", b"c", b"0", b"-"])
.starts_with("-NOGROUP")
);
}
#[test]
fn xdelex_answers_one_integer_an_id() {
let mut f = Fixture::new();
for i in 1..=4 {
f.run(&[b"XADD", b"s", format!("{i}-1").as_bytes(), b"a", b"1"]);
}
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"0"]);
f.run(&[
b"XREADGROUP",
b"GROUP",
b"g",
b"c",
b"COUNT",
b"2",
b"STREAMS",
b"s",
b">",
]);
assert_eq!(
f.run(&[b"XDELEX", b"s", b"IDS", b"2", b"1-1", b"9-9"]),
"*2\r\n:1\r\n:-1\r\n"
);
assert!(
f.run(&[b"XPENDING", b"s", b"g"])
.starts_with("*4\r\n:2\r\n")
);
assert_eq!(
f.run(&[b"XDELEX", b"s", b"DELREF", b"IDS", b"1", b"2-1"]),
"*1\r\n:1\r\n"
);
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g"]),
"*4\r\n:1\r\n$3\r\n1-1\r\n$3\r\n1-1\r\n*1\r\n*2\r\n$1\r\nc\r\n$1\r\n1\r\n"
);
assert_eq!(
f.run(&[b"XDELEX", b"s", b"ACKED", b"IDS", b"2", b"3-1", b"4-1"]),
"*2\r\n:2\r\n:2\r\n"
);
assert_eq!(
f.run(&[b"XDELEX", b"nope", b"IDS", b"2", b"bad", b"worse"]),
"*2\r\n:-1\r\n:-1\r\n"
);
assert!(
f.run(&[b"XDELEX", b"s", b"IDS", b"2", b"3-1", b"bad"])
.starts_with("-ERR Invalid stream ID")
);
assert_eq!(f.run(&[b"XLEN", b"s"]), ":2\r\n");
assert!(
f.run(&[b"XDELEX", b"s", b"IDS", b"0", b"1-1"])
.contains("Number of IDs must be a positive integer")
);
assert!(
f.run(&[b"XDELEX", b"s", b"IDS", b"2", b"1-1"])
.contains("The `numids` parameter must match the number of arguments")
);
assert!(
f.run(&[b"XDELEX", b"s", b"KEEPREF", b"DELREF", b"IDS", b"1", b"1-1"])
.starts_with("-ERR syntax error")
);
assert!(
f.run(&[b"XDELEX", b"s", b"IDS", b"1", b"1-1", b"2-1"])
.starts_with("-ERR syntax error")
);
f.run(&[b"SET", b"str", b"v"]);
assert!(
f.run(&[b"XDELEX", b"str", b"BOGUS", b"IDS", b"0", b"1-1"])
.starts_with("-WRONGTYPE")
);
}
#[test]
fn xackdel_reports_what_the_group_was_holding() {
let mut f = Fixture::new();
for i in 1..=3 {
f.run(&[b"XADD", b"s", format!("{i}-1").as_bytes(), b"a", b"1"]);
}
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"0"]);
f.run(&[
b"XREADGROUP",
b"GROUP",
b"g",
b"c",
b"COUNT",
b"1",
b"STREAMS",
b"s",
b">",
]);
assert_eq!(
f.run(&[b"XACKDEL", b"s", b"g", b"IDS", b"2", b"1-1", b"2-1"]),
"*2\r\n:1\r\n:-1\r\n"
);
assert_eq!(f.run(&[b"XLEN", b"s"]), ":2\r\n");
assert_eq!(
f.run(&[b"XACKDEL", b"s", b"nope", b"IDS", b"1", b"2-1"]),
"*1\r\n:-1\r\n"
);
assert_eq!(
f.run(&[b"XACKDEL", b"nope", b"g", b"IDS", b"1", b"2-1"]),
"*1\r\n:-1\r\n"
);
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c", b"STREAMS", b"s", b">"]);
f.run(&[b"XGROUP", b"CREATE", b"s", b"g2", b"0"]);
assert_eq!(
f.run(&[b"XACKDEL", b"s", b"g", b"ACKED", b"IDS", b"1", b"2-1"]),
"*1\r\n:2\r\n"
);
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g"]),
"*4\r\n:1\r\n$3\r\n3-1\r\n$3\r\n3-1\r\n*1\r\n*2\r\n$1\r\nc\r\n$1\r\n1\r\n"
);
assert_eq!(f.run(&[b"XLEN", b"s"]), ":2\r\n");
}
#[test]
fn xnack_releases_an_entry_for_the_next_claim() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
f.run(&[b"XADD", b"s", b"2-1", b"a", b"2"]);
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"0"]);
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c1", b"STREAMS", b"s", b">"]);
f.run(&[b"XCLAIM", b"s", b"g", b"c1", b"0", b"1-1", b"2-1"]);
assert_eq!(
f.run(&[b"XNACK", b"s", b"g", b"FAIL", b"IDS", b"1", b"1-1"]),
":1\r\n"
);
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10"]),
"*2\r\n*4\r\n$3\r\n1-1\r\n$0\r\n\r\n:-1\r\n:2\r\n*4\r\n$3\r\n2-1\r\n$2\r\nc1\r\n:0\r\n:2\r\n"
);
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10", b"c1"]),
"*1\r\n*4\r\n$3\r\n2-1\r\n$2\r\nc1\r\n:0\r\n:2\r\n"
);
assert_eq!(
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c2", b"STREAMS", b"s", b">"]),
"*-1\r\n"
);
assert_eq!(
f.run(&[
b"XAUTOCLAIM",
b"s",
b"g",
b"c2",
b"99999999",
b"-",
b"JUSTID"
]),
"*3\r\n$3\r\n0-0\r\n*1\r\n$3\r\n1-1\r\n*0\r\n"
);
f.run(&[b"XNACK", b"s", b"g", b"SILENT", b"IDS", b"1", b"1-1"]);
assert!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10"])
.contains(":-1\r\n:1\r\n")
);
f.run(&[b"XNACK", b"s", b"g", b"SILENT", b"IDS", b"1", b"1-1"]);
f.run(&[b"XNACK", b"s", b"g", b"SILENT", b"IDS", b"1", b"1-1"]);
assert!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10"])
.contains(":-1\r\n:0\r\n")
);
f.run(&[b"XNACK", b"s", b"g", b"FATAL", b"IDS", b"1", b"1-1"]);
assert!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10"])
.contains(":9223372036854775807\r\n")
);
f.run(&[
b"XNACK",
b"s",
b"g",
b"FATAL",
b"IDS",
b"1",
b"1-1",
b"RETRYCOUNT",
b"3",
]);
assert!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10"])
.contains(":-1\r\n:3\r\n")
);
f.run(&[b"XACK", b"s", b"g", b"2-1"]);
assert_eq!(
f.run(&[b"XNACK", b"s", b"g", b"FAIL", b"IDS", b"1", b"2-1"]),
":0\r\n"
);
assert_eq!(
f.run(&[
b"XNACK", b"s", b"g", b"FAIL", b"IDS", b"1", b"2-1", b"FORCE"
]),
":1\r\n"
);
assert!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10"])
.contains(":-1\r\n:0\r\n")
);
assert_eq!(
f.run(&[
b"XNACK", b"s", b"g", b"FAIL", b"IDS", b"1", b"9-9", b"FORCE"
]),
":0\r\n"
);
assert_eq!(
f.run(&[b"XNACK", b"s", b"nope", b"BOGUS", b"IDS", b"1", b"1-1"]),
"-NOGROUP No such key 's' or consumer group 'nope'\r\n"
);
assert!(
f.run(&[b"XNACK", b"s", b"g", b"BOGUS", b"IDS", b"1", b"1-1"])
.starts_with("-ERR")
);
assert!(
f.run(&[b"XNACK", b"s", b"g", b"FAIL", b"IDS", b"0", b"1-1"])
.contains("numids must be a positive integer")
);
assert!(
f.run(&[b"XNACK", b"s", b"g", b"FAIL", b"IDS", b"2", b"1-1"])
.contains("number of IDs doesn't match numids")
);
assert!(
f.run(&[b"XNACK", b"s", b"g", b"FAIL", b"IDS", b"1", b"1-1", b"2-1"])
.contains("Unrecognized XNACK option '2-1'")
);
}
#[test]
fn xinfo_reports_the_stream_the_groups_and_the_consumers() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
f.run(&[b"XADD", b"s", b"2-1", b"a", b"2"]);
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"0"]);
f.run(&[
b"XREADGROUP",
b"GROUP",
b"g",
b"c1",
b"COUNT",
b"1",
b"STREAMS",
b"s",
b">",
]);
let info = f.run(&[b"XINFO", b"STREAM", b"s"]);
assert!(info.starts_with("*20\r\n"), "{info}");
assert!(info.contains("$6\r\nlength\r\n:2\r\n"), "{info}");
assert!(
info.contains("$17\r\nlast-generated-id\r\n$3\r\n2-1\r\n"),
"{info}"
);
assert!(info.contains("$13\r\nentries-added\r\n:2\r\n"), "{info}");
assert!(info.contains("$6\r\ngroups\r\n:1\r\n"), "{info}");
let groups = f.run(&[b"XINFO", b"GROUPS", b"s"]);
assert!(groups.starts_with("*1\r\n*12\r\n"), "{groups}");
assert!(groups.contains("$9\r\nconsumers\r\n:1\r\n"), "{groups}");
assert!(groups.contains("$7\r\npending\r\n:1\r\n"), "{groups}");
assert!(groups.contains("$3\r\nlag\r\n:1\r\n"), "{groups}");
f.run(&[b"XGROUP", b"CREATECONSUMER", b"s", b"g", b"c2"]);
let consumers = f.run(&[b"XINFO", b"CONSUMERS", b"s", b"g"]);
assert!(consumers.starts_with("*2\r\n"), "{consumers}");
assert!(
consumers.contains("$8\r\ninactive\r\n:-1\r\n"),
"{consumers}"
);
let c1 = consumers.find("c1").unwrap();
let c2 = consumers.find("c2").unwrap();
assert!(c1 < c2, "{consumers}");
let full = f.run(&[b"XINFO", b"STREAM", b"s", b"FULL"]);
assert!(full.starts_with("*18\r\n"), "{full}");
assert!(full.contains("$12\r\nnacked-count\r\n:0\r\n"), "{full}");
assert!(full.contains("$11\r\nactive-time\r\n"), "{full}");
assert!(
f.run(&[b"XINFO", b"STREAM", b"missing"])
.contains("no such key")
);
assert!(
f.run(&[b"XINFO", b"GROUPS", b"missing"])
.contains("no such key")
);
assert!(
f.run(&[b"XINFO", b"CONSUMERS", b"s", b"nope"])
.starts_with("-NOGROUP")
);
assert!(
f.run(&[b"XINFO", b"NOSUCH", b"s"])
.contains("Try XINFO HELP")
);
assert!(f.run(&[b"XINFO", b"HELP"]).contains("XINFO <subcommand>"));
assert!(f.run(&[b"XGROUP", b"HELP"]).contains("XGROUP <subcommand>"));
}
#[test]
fn xpending_takes_the_consumer_only_when_the_count_comes_out_right() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
f.run(&[b"XGROUP", b"CREATE", b"s", b"g", b"0"]);
f.run(&[b"XREADGROUP", b"GROUP", b"g", b"c1", b"STREAMS", b"s", b">"]);
let list = f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10"]);
assert_eq!(list, "*1\r\n*4\r\n$3\r\n1-1\r\n$2\r\nc1\r\n:0\r\n:1\r\n");
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10", b"c1"]),
"*1\r\n*4\r\n$3\r\n1-1\r\n$2\r\nc1\r\n:0\r\n:1\r\n"
);
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"10", b"nope"]),
"*0\r\n"
);
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g", b"IDLE", b"0", b"-", b"+", b"10"]),
list
);
assert!(
f.run(&[b"XPENDING", b"s", b"g", b"IDLE", b"0"])
.contains("syntax error")
);
assert!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+"])
.contains("syntax error")
);
assert_eq!(
f.run(&[b"XPENDING", b"s", b"g", b"-", b"+", b"-1"]),
"*0\r\n"
);
assert!(
f.run(&[b"XPENDING", b"missing", b"g"])
.starts_with("-NOGROUP")
);
}
#[test]
fn xsetid_will_not_go_below_what_is_there() {
let mut f = Fixture::new();
f.run(&[b"XADD", b"s", b"5-5", b"a", b"1"]);
assert_eq!(f.run(&[b"XSETID", b"s", b"9-9"]), "+OK\r\n");
assert_eq!(
f.run(&[
b"XSETID",
b"s",
b"10-1",
b"ENTRIESADDED",
b"7",
b"MAXDELETEDID",
b"9-1"
]),
"+OK\r\n"
);
let info = f.run(&[b"XINFO", b"STREAM", b"s"]);
assert!(info.contains("$13\r\nentries-added\r\n:7\r\n"), "{info}");
assert!(
info.contains("$20\r\nmax-deleted-entry-id\r\n$3\r\n9-1\r\n"),
"{info}"
);
assert!(
f.run(&[b"XSETID", b"s", b"1-1"])
.contains("smaller than the target stream top item")
);
assert!(
f.run(&[b"XSETID", b"s", b"10-1", b"ENTRIESADDED", b"-1"])
.contains("entries_added must be positive")
);
assert!(
f.run(&[b"XSETID", b"missing", b"1-1"])
.contains("no such key")
);
}
#[test]
fn xread_answers_a_map_on_resp3_and_the_fields_stay_flat() {
let mut f = Fixture::new();
f.run(&[b"HELLO", b"3"]);
f.run(&[b"XADD", b"s", b"1-1", b"a", b"1"]);
assert_eq!(
f.run(&[b"XREAD", b"STREAMS", b"s", b"0"]),
"%1\r\n$1\r\ns\r\n*1\r\n*2\r\n$3\r\n1-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
assert_eq!(
f.run(&[b"XRANGE", b"s", b"-", b"+"]),
"*1\r\n*2\r\n$3\r\n1-1\r\n*2\r\n$1\r\na\r\n$1\r\n1\r\n"
);
assert_eq!(f.run(&[b"XREAD", b"STREAMS", b"s", b"1-1"]), "_\r\n");
}
struct Mem {
blobs: Vec<Vec<u8>>,
}
impl yo_kv::cold::Blocks for Mem {
fn put(&mut self, bytes: &[u8]) -> yo_common::Result<yo_common::Addr> {
self.blobs.push(bytes.to_vec());
Ok(yo_common::Addr::new(
yo_common::Space::Log,
(self.blobs.len() - 1) as u64,
))
}
fn get(&self, at: yo_common::Addr) -> yo_common::Result<&[u8]> {
self.blobs
.get(at.offset() as usize)
.map(Vec::as_slice)
.ok_or_else(|| {
yo_common::Error::new(yo_common::Code::Corrupt, "no chunk at that address")
})
}
fn bytes(&self) -> u64 {
self.blobs.iter().map(|b| b.len() as u64).sum()
}
}
fn filled(attach: bool) -> (Fixture, usize) {
let mut f = Fixture::new();
if attach {
f.server.db(0).attach(Box::new(Mem { blobs: Vec::new() }));
}
let val = vec![b'v'; 256];
for i in 0..24000u32 {
let k = format!("key:{i:08}");
f.run(&[b"SET", k.as_bytes(), &val]);
}
let full = f.server.memory_bytes();
assert!(full > 3 * 1024 * 1024, "the arena is several segments");
(f, full)
}
fn press(f: &mut Fixture, limit: usize) {
let val = vec![b'v'; 256];
for i in 0..3000u32 {
let k = format!("new:{i:08}");
assert_eq!(
f.run(&[b"SET", k.as_bytes(), &val]),
"+OK\r\n",
"write {i} was refused"
);
f.server.refresh_memory();
if f.server.memory_bytes() <= limit {
return;
}
}
panic!(
"it never got under: {} against {limit}",
f.server.memory_bytes()
);
}
#[test]
fn the_storage_limit_reads_back_and_minus_one_is_no_limit() {
let mut f = Fixture::new();
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"maxstore"]),
"*2\r\n$8\r\nmaxstore\r\n$2\r\n-1\r\n",
"no limit is the default"
);
for (typed, bytes) in [
(&b"0"[..], "0"),
(b"1024", "1024"),
(b"1k", "1000"),
(b"1gb", "1073741824"),
(b"-1", "-1"),
] {
assert_eq!(f.run(&[b"CONFIG", b"SET", b"maxstore", typed]), "+OK\r\n");
assert_eq!(
f.run(&[b"CONFIG", b"GET", b"maxstore"]),
format!("*2\r\n$8\r\nmaxstore\r\n${}\r\n{bytes}\r\n", bytes.len()),
"set {}",
String::from_utf8_lossy(typed)
);
}
for bad in [&b"1tb"[..], b"-2", b"", b"lots"] {
assert_eq!(
f.run(&[b"CONFIG", b"SET", b"maxstore", bad]),
"-ERR CONFIG SET failed (possibly related to argument 'maxstore') - argument must be a memory value or -1\r\n",
"refused {}",
String::from_utf8_lossy(bad)
);
}
let info = f.run(&[b"INFO", b"memory"]);
assert!(info.contains("maxstore:-1"), "{info}");
assert!(info.contains("yo_memory_regime:evict"), "{info}");
assert!(info.contains("yo_store_bytes:0"), "{info}");
}
#[test]
fn a_memory_limit_moves_values_to_the_file_instead_of_dropping_keys() {
let (mut f, full) = filled(true);
let keys = f.run(&[b"DBSIZE"]);
assert!(
f.run(&[b"INFO", b"memory"])
.contains("yo_memory_regime:migrate"),
"a database with somewhere to put values migrates"
);
let limit = full - 2 * 1024 * 1024;
f.run(&[b"CONFIG", b"SET", b"maxmemory-policy", b"allkeys-lru"]);
f.run(&[
b"CONFIG",
b"SET",
b"maxmemory",
limit.to_string().as_bytes(),
]);
press(&mut f, limit);
assert!(
f.run(&[b"INFO", b"stats"]).contains("evicted_keys:0"),
"nothing was thrown away"
);
let after: usize = f.run(&[b"DBSIZE"])[1..]
.trim_end()
.parse()
.expect("a count");
let before: usize = keys[1..].trim_end().parse().expect("a count");
assert!(after > before, "the keys that came in are all still here");
assert!(
f.server.store_bytes() > 0,
"and what came out of memory went to the file"
);
let val = format!("$256\r\n{}\r\n", "v".repeat(256));
assert_eq!(f.run(&[b"GET", b"key:00000000"]), val);
assert_eq!(f.run(&[b"GET", b"key:00023999"]), val);
}
#[test]
fn a_storage_limit_of_zero_restores_redis_behaviour_exactly() {
let (mut f, full) = filled(true);
f.run(&[b"CONFIG", b"SET", b"maxstore", b"0"]);
assert!(
f.run(&[b"INFO", b"memory"])
.contains("yo_memory_regime:evict"),
"nothing may go to the file"
);
let limit = full - 2 * 1024 * 1024;
f.run(&[b"CONFIG", b"SET", b"maxmemory-policy", b"allkeys-lru"]);
f.run(&[
b"CONFIG",
b"SET",
b"maxmemory",
limit.to_string().as_bytes(),
]);
press(&mut f, limit);
assert!(
!f.run(&[b"INFO", b"stats"]).contains("evicted_keys:0"),
"keys were thrown away, which is what was asked for"
);
assert_eq!(f.server.store_bytes(), 0, "and the file was never written");
}
#[test]
fn a_full_file_goes_back_to_evicting() {
let (mut f, full) = filled(true);
f.run(&[b"CONFIG", b"SET", b"maxstore", b"64kb"]);
let limit = full - 2 * 1024 * 1024;
f.run(&[b"CONFIG", b"SET", b"maxmemory-policy", b"allkeys-lru"]);
f.run(&[
b"CONFIG",
b"SET",
b"maxmemory",
limit.to_string().as_bytes(),
]);
press(&mut f, limit);
assert!(f.server.store_bytes() >= 64 * 1024, "the file filled up");
assert!(
!f.run(&[b"INFO", b"stats"]).contains("evicted_keys:0"),
"and then it started evicting"
);
assert!(
f.run(&[b"INFO", b"memory"])
.contains("yo_memory_regime:evict"),
"and it says so"
);
}
}