diff --git a/rueidiscompat/adapter.go b/rueidiscompat/adapter.go index ae55744f..4a2dc528 100644 --- a/rueidiscompat/adapter.go +++ b/rueidiscompat/adapter.go @@ -2924,7 +2924,7 @@ func (c *Compat) ClientKillByFilter(ctx context.Context, keys ...string) *IntCmd func (c *Compat) ClientList(ctx context.Context) *StringCmd { cmd := c.client.B().ClientList().Build() resp := c.client.Do(ctx, cmd) - return newStringCmd(resp) + return newTxtStringCmd(resp) } func (c *Compat) ClientPause(ctx context.Context, dur time.Duration) *BoolCmd { @@ -3030,7 +3030,7 @@ func (c *Compat) FlushDBAsync(ctx context.Context) *StatusCmd { func (c *Compat) Info(ctx context.Context, section ...string) *StringCmd { cmd := c.client.B().Info().Section(section...).Build() resp := c.client.Do(ctx, cmd) - return newStringCmd(resp) + return newTxtStringCmd(resp) } // InfoMap returns INFO parsed into sections as map[string]map[string]string @@ -3343,7 +3343,7 @@ func (c *Compat) ClusterShards(ctx context.Context) *ClusterShardsCmd { func (c *Compat) ClusterNodes(ctx context.Context) *StringCmd { cmd := c.client.B().ClusterNodes().Build() resp := c.client.Do(ctx, cmd) - return newStringCmd(resp) + return newTxtStringCmd(resp) } func (c *Compat) ClusterLinks(ctx context.Context) *ClusterLinksCmd { @@ -3385,7 +3385,7 @@ func (c *Compat) ClusterResetHard(ctx context.Context) *StatusCmd { func (c *Compat) ClusterInfo(ctx context.Context) *StringCmd { cmd := c.client.B().ClusterInfo().Build() resp := c.client.Do(ctx, cmd) - return newStringCmd(resp) + return newTxtStringCmd(resp) } func (c *Compat) ClusterKeySlot(ctx context.Context, key string) *IntCmd { diff --git a/rueidiscompat/adapter_test.go b/rueidiscompat/adapter_test.go index d9defca0..fd4f03fb 100644 --- a/rueidiscompat/adapter_test.go +++ b/rueidiscompat/adapter_test.go @@ -203,7 +203,11 @@ func testCluster(resp3 bool) { It("ClusterNodes", func() { nodes, err := adapter.ClusterNodes(ctx).Result() Expect(err).NotTo(HaveOccurred()) - Expect(strings.Split(strings.TrimSpace(nodes), "\n")).To(HaveLen(3)) + lines := strings.Split(strings.TrimSpace(nodes), "\n") + Expect(lines).To(HaveLen(3)) + for _, line := range lines { + Expect(line).To(MatchRegexp("^[0-9a-f]{40} ")) + } }) It("ClusterMeet", func() { Expect(adapter.ClusterMeet(ctx, "localhost", 8080).Err()).To(MatchError("Invalid node address specified: localhost:8080")) @@ -217,7 +221,7 @@ func testCluster(resp3 bool) { It("ClusterInfo", func() { info, err := adapter.ClusterInfo(ctx).Result() Expect(err).NotTo(HaveOccurred()) - Expect(info).NotTo(BeEmpty()) + Expect(info).To(HavePrefix("cluster_state:")) }) It("ClusterKeySlot", func() { slot, err := adapter.ClusterKeySlot(ctx, "1").Result() @@ -318,7 +322,7 @@ func testAdapter(resp3 bool) { It("should ClientList", func() { r := adapter.ClientList(ctx) Expect(r.Err()).To(BeNil()) - Expect(r.Val()).NotTo(Equal("")) + Expect(r.Val()).To(HavePrefix("id=")) }) It("should ClientID", func() { @@ -365,7 +369,7 @@ func testAdapter(resp3 bool) { It("should Info", func() { info := adapter.Info(ctx) Expect(info.Err()).NotTo(HaveOccurred()) - Expect(info.Val()).NotTo(Equal("")) + Expect(info.Val()).To(HavePrefix("# Server")) }) It("should InfoMap", func() { @@ -380,7 +384,7 @@ func testAdapter(resp3 bool) { It("should Info cpu", func() { info := adapter.Info(ctx, "cpu") Expect(info.Err()).NotTo(HaveOccurred()) - Expect(info.Val()).NotTo(Equal("")) + Expect(info.Val()).To(HavePrefix("# CPU")) Expect(info.Val()).To(ContainSubstring(`used_cpu_sys`)) }) diff --git a/rueidiscompat/command.go b/rueidiscompat/command.go index 19b53af0..1f5c4dd0 100644 --- a/rueidiscompat/command.go +++ b/rueidiscompat/command.go @@ -359,6 +359,8 @@ func (cmd *Cmd) BoolSlice() ([]bool, error) { type StringCmd struct { baseCmd[string] + // txt drops the "txt:" format prefix of a RESP3 verbatim string reply + txt bool } type ZeroCopyStringCmd struct { @@ -378,6 +380,9 @@ func (cmd *StringCmd) from(res rueidis.RedisResult) { err = nil } } + if cmd.txt && err == nil { + val = strings.TrimPrefix(val, "txt:") + } cmd.SetErr(err) cmd.SetVal(val) cmd.setIsCacheHit(res.IsCacheHit()) @@ -389,6 +394,14 @@ func newStringCmd(res rueidis.RedisResult) *StringCmd { return cmd } +// newTxtStringCmd is for commands like INFO and CLIENT LIST, which reply with +// a verbatim string in RESP3. Its value doesn't start with "txt:", as in RESP2. +func newTxtStringCmd(res rueidis.RedisResult) *StringCmd { + cmd := &StringCmd{txt: true} + cmd.from(res) + return cmd +} + func (cmd *StringCmd) Bytes() ([]byte, error) { return []byte(cmd.val), cmd.err } diff --git a/rueidiscompat/command_test.go b/rueidiscompat/command_test.go index 0421b3f7..8774cb0e 100644 --- a/rueidiscompat/command_test.go +++ b/rueidiscompat/command_test.go @@ -1572,6 +1572,47 @@ func TestZSliceSingleCmdFrom(t *testing.T) { }) } +func TestTxtStringCmdFrom(t *testing.T) { + t.Parallel() + + // RESP3 verbatim strings carry a "txt:" format prefix. The mock package has + // no verbatim string, so a blob string with the same content stands in for it. + t.Run("reply with the prefix", func(t *testing.T) { + cmd := newTxtStringCmd(mock.Result(mock.RedisBlobString("txt:# Server\r\nredis_version:8.2.1\r\n"))) + if err := cmd.Err(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got, want := cmd.Val(), "# Server\r\nredis_version:8.2.1\r\n"; got != want { + t.Fatalf("got %q, want %q", got, want) + } + }) + + t.Run("reply without the prefix", func(t *testing.T) { + cmd := newTxtStringCmd(mock.Result(mock.RedisBlobString("id=1 addr=127.0.0.1:6379\n"))) + if err := cmd.Err(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got, want := cmd.Val(), "id=1 addr=127.0.0.1:6379\n"; got != want { + t.Fatalf("got %q, want %q", got, want) + } + }) + + t.Run("newStringCmd keeps the value", func(t *testing.T) { + cmd := newStringCmd(mock.Result(mock.RedisBlobString("txt:hello"))) + if got, want := cmd.Val(), "txt:hello"; got != want { + t.Fatalf("got %q, want %q", got, want) + } + }) + + t.Run("error", func(t *testing.T) { + err := errors.New("any") + cmd := newTxtStringCmd(mock.ErrorResult(err)) + if cmd.Err() != err { + t.Fatalf("got error %v, want %v", cmd.Err(), err) + } + }) +} + // TestCacheHitCmd tests the IsCacheHit and setIsCacheHit functionality for Cmd func TestCacheHitCmd(t *testing.T) { cmd := &Cmd{} diff --git a/rueidiscompat/pipeline_test.go b/rueidiscompat/pipeline_test.go index 7f2e9666..0bdc0fa5 100644 --- a/rueidiscompat/pipeline_test.go +++ b/rueidiscompat/pipeline_test.go @@ -119,6 +119,16 @@ func testAdapterPipeline(resp3 bool) { Expect(reset.Err()).NotTo(HaveOccurred()) }) + It("should Pipeline Info and ClientList", func() { + pipe := adapter.Pipeline() + info := pipe.Info(ctx) + clients := pipe.ClientList(ctx) + _, err := pipe.Exec(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Val()).To(HavePrefix("# Server")) + Expect(clients.Val()).To(HavePrefix("id=")) + }) + It("should panic for GetToBuffer in Pipeline", func() { pipe := adapter.Pipeline()