diff --git a/README.md b/README.md index 2a065248..a6c2825f 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Implemented commands: - RPOPLPUSH - RPUSH - RPUSHX + - LMOVE - Pub/Sub (complete) - PSUBSCRIBE - PUBLISH diff --git a/cmd_list.go b/cmd_list.go index 364305c2..62f96918 100644 --- a/cmd_list.go +++ b/cmd_list.go @@ -36,6 +36,7 @@ func commandsList(m *Miniredis) { m.srv.Register("RPOPLPUSH", m.cmdRpoplpush) m.srv.Register("RPUSH", m.cmdRpush) m.srv.Register("RPUSHX", m.cmdRpushx) + m.srv.Register("LMOVE", m.cmdLmove) } // BLPOP @@ -771,3 +772,52 @@ func (m *Miniredis) cmdBrpoplpush(c *server.Peer, cmd string, args []string) { }, ) } + +// LMOVE +func (m *Miniredis) cmdLmove(c *server.Peer, cmd string, args []string) { + if len(args) != 4 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + src, dst, srcDir, dstDir := args[0], args[1], strings.ToLower(args[2]), strings.ToLower(args[3]) + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(src) { + c.WriteNull() + return + } + if db.t(src) != "list" || (db.exists(dst) && db.t(dst) != "list") { + c.WriteError(msgWrongType) + return + } + var elem string + if srcDir == "left" { + elem = db.listLpop(src) + } else if srcDir == "right" { + elem = db.listPop(src) + } else { + c.WriteError(msgSyntaxError) + return + } + + if dstDir == "left" { + db.listLpush(dst, elem) + } else if dstDir == "right" { + db.listPush(dst, elem) + } else { + c.WriteError(msgSyntaxError) + return + } + c.WriteBulk(elem) + }) +} diff --git a/cmd_list_test.go b/cmd_list_test.go index 4a5b88be..dd9dec41 100644 --- a/cmd_list_test.go +++ b/cmd_list_test.go @@ -1274,3 +1274,134 @@ func TestBrpoplpushTimeout(t *testing.T) { t.Error("BRPOPLPUSH took too long") } } + +func TestLmove(t *testing.T) { + s, err := Run() + ok(t, err) + defer s.Close() + c, err := proto.Dial(s.Addr()) + ok(t, err) + defer c.Close() + + s.Push("src", "LR", "LL", "RR", "RL") + s.Push("dst", "m1", "m2", "m3") + // RIGHT LEFT + { + mustDo(t, c, + "LMOVE", "src", "dst", "RIGHT", "LEFT", + proto.String("RL"), + ) + s.CheckList(t, "src", "LR", "LL", "RR") + s.CheckList(t, "dst", "RL", "m1", "m2", "m3") + } + // LEFT RIGHT + { + mustDo(t, c, + "LMOVE", "src", "dst", "LEFT", "RIGHT", + proto.String("LR"), + ) + s.CheckList(t, "src", "LL", "RR") + s.CheckList(t, "dst", "RL", "m1", "m2", "m3", "LR") + } + // RIGHT RIGHT + { + mustDo(t, c, + "LMOVE", "src", "dst", "RIGHT", "RIGHT", + proto.String("RR"), + ) + s.CheckList(t, "src", "LL") + s.CheckList(t, "dst", "RL", "m1", "m2", "m3", "LR", "RR") + } + // LEFT LEFT + { + mustDo(t, c, + "LMOVE", "src", "dst", "LEFT", "LEFT", + proto.String("LL"), + ) + assert(t, !s.Exists("src"), "src exists") + s.CheckList(t, "dst", "LL", "RL", "m1", "m2", "m3", "LR", "RR") + } + + // Non exising lists + { + s.Push("ll", "aap", "noot", "mies") + + mustDo(t, c, + "LMOVE", "ll", "nosuch", "RIGHT", "LEFT", + proto.String("mies"), + ) + assert(t, s.Exists("nosuch"), "nosuch exists") + s.CheckList(t, "ll", "aap", "noot") + s.CheckList(t, "nosuch", "mies") + + mustNil(t, c, + "LMOVE", "nosuch2", "ll", "RIGHT", "LEFT", + ) + } + + // Cycle + { + s.Push("cycle", "aap", "noot", "mies") + + mustDo(t, c, + "LMOVE", "cycle", "cycle", "RIGHT", "LEFT", + proto.String("mies"), + ) + s.CheckList(t, "cycle", "mies", "aap", "noot") + + mustDo(t, c, + "LMOVE", "cycle", "cycle", "LEFT", "RIGHT", + proto.String("mies"), + ) + s.CheckList(t, "cycle", "aap", "noot", "mies") + } + + // Error cases + t.Run("errors", func(t *testing.T) { + s.Push("src", "aap", "noot", "mies") + s.Push("dst", "aap", "noot", "mies") + mustDo(t, c, + "LMOVE", + proto.Error(errWrongNumber("lmove")), + ) + mustDo(t, c, + "LMOVE", "l", + proto.Error(errWrongNumber("lmove")), + ) + mustDo(t, c, + "LMOVE", "l", "l", + proto.Error(errWrongNumber("lmove")), + ) + mustDo(t, c, + "LMOVE", "l", "l", "l", + proto.Error(errWrongNumber("lmove")), + ) + mustDo(t, c, + "LMOVE", "too", "many", "many", "many", "arguments", + proto.Error(errWrongNumber("lmove")), + ) + + s.Set("str", "string!") + mustDo(t, c, + "LMOVE", "str", "src", "left", "right", + proto.Error(msgWrongType), + ) + mustDo(t, c, + "LMOVE", "src", "str", "left", "right", + proto.Error(msgWrongType), + ) + + mustDo(t, c, + "LMOVE", "src", "dst", "no", "good", + proto.Error("ERR syntax error"), + ) + mustDo(t, c, + "LMOVE", "src", "dst", "invalid", "right", + proto.Error("ERR syntax error"), + ) + mustDo(t, c, + "LMOVE", "src", "dst", "left", "invalid", + proto.Error("ERR syntax error"), + ) + }) +} diff --git a/integration/list_test.go b/integration/list_test.go index 0651bdc1..4389d716 100644 --- a/integration/list_test.go +++ b/integration/list_test.go @@ -455,3 +455,52 @@ func TestBrpoplpush(t *testing.T) { }, ) } + +func TestLmove(t *testing.T) { + testRaw(t, func(c *client) { + c.Do("RPUSH", "src", "LR", "LL", "RR", "RL") + c.Do("LMOVE", "src", "dst", "LEFT", "RIGHT") + c.Do("LRANGE", "src", "0", "-1") + c.Do("LRANGE", "dst", "0", "-1") + c.Do("LMOVE", "src", "dst", "RIGHT", "LEFT") + c.Do("LMOVE", "src", "dst", "LEFT", "LEFT") + c.Do("LMOVE", "src", "dst", "RIGHT", "RIGHT") // now empty + c.Do("EXISTS", "src") + c.Do("LRANGE", "dst", "0", "-1") + + // Cycle left to right + c.Do("RPUSH", "round", "aap", "noot", "mies") + c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") + c.Do("LRANGE", "round", "0", "-1") + c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") + c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") + c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") + c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") + c.Do("LRANGE", "round", "0", "-1") + // Cycle right to left + c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") + c.Do("LRANGE", "round", "0", "-1") + c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") + c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") + c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") + c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") + c.Do("LRANGE", "round", "0", "-1") + // Cycle same side + c.Do("LMOVE", "round", "round", "LEFT", "LEFT") + c.Do("LRANGE", "round", "0", "-1") + c.Do("LMOVE", "round", "round", "RIGHT", "RIGHT") + c.Do("LRANGE", "round", "0", "-1") + + // failure cases + c.Do("RPUSH", "chk", "aap", "noot", "mies") + c.Error("wrong number", "LMOVE") + c.Error("wrong number", "LMOVE", "chk") + c.Error("wrong number", "LMOVE", "chk", "dst") + c.Error("wrong number", "LMOVE", "chk", "dst", "chk") + c.Error("wrong number", "LMOVE", "chk", "dst", "chk", "too", "many") + c.Do("SET", "str", "I am a string") + c.Error("wrong kind", "LMOVE", "chk", "str", "LEFT", "LEFT") + c.Error("wrong kind", "LMOVE", "str", "chk", "LEFT", "LEFT") + c.Do("LRANGE", "chk", "0", "-1") + }) +}