mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-21 16:34:21 +08:00
🐛 fix(redis): 修复列表新增与精确删除
- 对齐新版 Redis List 头尾新增的 Wails 参数契约 - 分别使用 LPUSH 与 RPUSH 保证插入位置语义 - 按索引原子删除并避免重复值误删 - 补充多语言文案与前后端回归测试 Refs #639
This commit is contained in:
@@ -54,6 +54,11 @@ type RedisImportKeysOptions struct {
|
||||
File string `json:"file,omitempty"`
|
||||
}
|
||||
|
||||
type RedisListPushOptions struct {
|
||||
Values []string `json:"values"`
|
||||
Position string `json:"position"`
|
||||
}
|
||||
|
||||
type RedisImportPreview struct {
|
||||
File string `json:"file"`
|
||||
ExportedAt string `json:"exportedAt,omitempty"`
|
||||
@@ -1379,14 +1384,31 @@ func (a *App) RedisDeleteHashField(config connection.ConnectionConfig, key strin
|
||||
}
|
||||
|
||||
// RedisListPush pushes values to a list
|
||||
func (a *App) RedisListPush(config connection.ConnectionConfig, key string, values []string) connection.QueryResult {
|
||||
func (a *App) RedisListPush(config connection.ConnectionConfig, key string, options RedisListPushOptions) connection.QueryResult {
|
||||
if len(options.Values) == 0 {
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("redis.backend.error.argument_required", map[string]any{"name": "values"}),
|
||||
}
|
||||
}
|
||||
if options.Position != "left" && options.Position != "right" {
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("redis.backend.error.list_position_invalid", nil),
|
||||
}
|
||||
}
|
||||
|
||||
config.Type = "redis"
|
||||
client, err := a.getRedisClient(config)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
if err := client.ListPush(key, values...); err != nil {
|
||||
push := client.ListPush
|
||||
if options.Position == "left" {
|
||||
push = client.ListPushLeft
|
||||
}
|
||||
if err := push(key, options.Values...); err != nil {
|
||||
logger.Error(err, "RedisListPush 添加失败:key=%s", key)
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
@@ -1410,16 +1432,22 @@ func (a *App) RedisListSet(config connection.ConnectionConfig, key string, index
|
||||
return connection.QueryResult{Success: true, Message: a.appText("redis.backend.message.set_success", nil)}
|
||||
}
|
||||
|
||||
// RedisListRemove removes one matching value from a list.
|
||||
func (a *App) RedisListRemove(config connection.ConnectionConfig, key, value string) connection.QueryResult {
|
||||
// RedisListRemove removes the expected value at an index in a list.
|
||||
func (a *App) RedisListRemove(config connection.ConnectionConfig, key string, index int64, value string) connection.QueryResult {
|
||||
config.Type = "redis"
|
||||
client, err := a.getRedisClient(config)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
if err := client.ListRemove(key, value); err != nil {
|
||||
logger.Error(err, "RedisListRemove 删除失败:key=%s", key)
|
||||
if err := client.ListRemoveAt(key, index, value); err != nil {
|
||||
if errors.Is(err, redis.ErrRedisListItemChanged) {
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("redis.backend.error.list_item_changed", nil),
|
||||
}
|
||||
}
|
||||
logger.Error(err, "RedisListRemove 删除失败:key=%s index=%d", key, index)
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"GoNavi-Wails/internal/connection"
|
||||
redislib "GoNavi-Wails/internal/redis"
|
||||
"GoNavi-Wails/shared/i18n"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func redisFunctionSource(t *testing.T, source string, signature string) string {
|
||||
@@ -23,7 +23,6 @@ func redisFunctionSource(t *testing.T, source string, signature string) string {
|
||||
return source[start : start+len(signature)+end]
|
||||
}
|
||||
|
||||
|
||||
func TestRedisBackendOperationMessageCatalogKeysExist(t *testing.T) {
|
||||
catalogs, err := i18n.LoadCatalogs()
|
||||
if err != nil {
|
||||
@@ -44,6 +43,8 @@ func TestRedisBackendOperationMessageCatalogKeysExist(t *testing.T) {
|
||||
"redis.backend.error.command_required",
|
||||
"redis.backend.error.argument_required",
|
||||
"redis.backend.error.argument_invalid_type",
|
||||
"redis.backend.error.list_item_changed",
|
||||
"redis.backend.error.list_position_invalid",
|
||||
"redis.backend.error.export_no_keys",
|
||||
"redis.backend.error.import_no_keys_selected",
|
||||
"redis.backend.error.import_payload_invalid",
|
||||
|
||||
@@ -12,19 +12,25 @@ import (
|
||||
)
|
||||
|
||||
type capturingRedisClient struct {
|
||||
connectConfig connection.ConnectionConfig
|
||||
deletedHashKey string
|
||||
deletedHashFields []string
|
||||
removedListKey string
|
||||
removedListValue string
|
||||
valueResult *redislib.RedisValue
|
||||
listResult []string
|
||||
listKey string
|
||||
listStart int64
|
||||
listStop int64
|
||||
listCalls int
|
||||
closed int
|
||||
closeErr error
|
||||
connectConfig connection.ConnectionConfig
|
||||
deletedHashKey string
|
||||
deletedHashFields []string
|
||||
listPushKey string
|
||||
listPushValues []string
|
||||
listPushLeftKey string
|
||||
listPushLeftValues []string
|
||||
removedListKey string
|
||||
removedListIndex int64
|
||||
removedListValue string
|
||||
listRemoveErr error
|
||||
valueResult *redislib.RedisValue
|
||||
listResult []string
|
||||
listKey string
|
||||
listStart int64
|
||||
listStop int64
|
||||
listCalls int
|
||||
closed int
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (c *capturingRedisClient) Connect(config connection.ConnectionConfig) error {
|
||||
@@ -86,14 +92,25 @@ func (c *capturingRedisClient) GetList(key string, start, stop int64) ([]string,
|
||||
return append([]string(nil), c.listResult...), nil
|
||||
}
|
||||
|
||||
func (c *capturingRedisClient) ListPush(key string, values ...string) error { return nil }
|
||||
func (c *capturingRedisClient) ListPush(key string, values ...string) error {
|
||||
c.listPushKey = key
|
||||
c.listPushValues = append([]string(nil), values...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *capturingRedisClient) ListPushLeft(key string, values ...string) error {
|
||||
c.listPushLeftKey = key
|
||||
c.listPushLeftValues = append([]string(nil), values...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *capturingRedisClient) ListSet(key string, index int64, value string) error { return nil }
|
||||
|
||||
func (c *capturingRedisClient) ListRemove(key, value string) error {
|
||||
func (c *capturingRedisClient) ListRemoveAt(key string, index int64, value string) error {
|
||||
c.removedListKey = key
|
||||
c.removedListIndex = index
|
||||
c.removedListValue = value
|
||||
return nil
|
||||
return c.listRemoveErr
|
||||
}
|
||||
|
||||
func (c *capturingRedisClient) GetSet(key string) ([]string, error) { return nil, nil }
|
||||
@@ -919,7 +936,161 @@ func TestRedisGetListValueDescendingReadsAndReversesTailWindow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisListRemoveDeletesOneValue(t *testing.T) {
|
||||
func TestRedisListPushAppendsValuesWithoutChangingThem(t *testing.T) {
|
||||
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
app.configDir = t.TempDir()
|
||||
|
||||
CloseAllRedisClients()
|
||||
client := &capturingRedisClient{}
|
||||
originalNewRedisClientFunc := newRedisClientFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
newRedisClientFunc = originalNewRedisClientFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
CloseAllRedisClients()
|
||||
}()
|
||||
newRedisClientFunc = func() redislib.RedisClient {
|
||||
return client
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
values := []string{" review ", "", "\t"}
|
||||
result := app.RedisListPush(connection.ConnectionConfig{
|
||||
Type: "redis",
|
||||
Host: "redis.local",
|
||||
Port: 6379,
|
||||
}, "tasks", RedisListPushOptions{
|
||||
Values: values,
|
||||
Position: "right",
|
||||
})
|
||||
if !result.Success {
|
||||
t.Fatalf("RedisListPush returned failure: %+v", result)
|
||||
}
|
||||
if client.listPushKey != "tasks" || !reflect.DeepEqual(client.listPushValues, values) {
|
||||
t.Fatalf("unexpected list push call: key=%q values=%#v", client.listPushKey, client.listPushValues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisListPushPrependsValues(t *testing.T) {
|
||||
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
app.configDir = t.TempDir()
|
||||
|
||||
CloseAllRedisClients()
|
||||
client := &capturingRedisClient{}
|
||||
originalNewRedisClientFunc := newRedisClientFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
newRedisClientFunc = originalNewRedisClientFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
CloseAllRedisClients()
|
||||
}()
|
||||
newRedisClientFunc = func() redislib.RedisClient {
|
||||
return client
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
values := []string{"review", "ship"}
|
||||
result := app.RedisListPush(connection.ConnectionConfig{
|
||||
Type: "redis",
|
||||
Host: "redis.local",
|
||||
Port: 6379,
|
||||
}, "tasks", RedisListPushOptions{
|
||||
Values: values,
|
||||
Position: "left",
|
||||
})
|
||||
if !result.Success {
|
||||
t.Fatalf("RedisListPush returned failure: %+v", result)
|
||||
}
|
||||
if client.listPushLeftKey != "tasks" || !reflect.DeepEqual(client.listPushLeftValues, values) {
|
||||
t.Fatalf("unexpected left list push call: key=%q values=%#v", client.listPushLeftKey, client.listPushLeftValues)
|
||||
}
|
||||
if client.listPushKey != "" || client.listPushValues != nil {
|
||||
t.Fatalf("right list push must not be used: key=%q values=%#v", client.listPushKey, client.listPushValues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisListPushRejectsEmptyValues(t *testing.T) {
|
||||
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
app.configDir = t.TempDir()
|
||||
|
||||
CloseAllRedisClients()
|
||||
client := &capturingRedisClient{}
|
||||
originalNewRedisClientFunc := newRedisClientFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
newRedisClientFunc = originalNewRedisClientFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
CloseAllRedisClients()
|
||||
}()
|
||||
newRedisClientFunc = func() redislib.RedisClient {
|
||||
return client
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
result := app.RedisListPush(connection.ConnectionConfig{
|
||||
Type: "redis",
|
||||
Host: "redis.local",
|
||||
Port: 6379,
|
||||
}, "tasks", RedisListPushOptions{Position: "right"})
|
||||
if result.Success {
|
||||
t.Fatalf("RedisListPush should reject empty values: %+v", result)
|
||||
}
|
||||
want := app.appText("redis.backend.error.argument_required", map[string]any{"name": "values"})
|
||||
if result.Message != want {
|
||||
t.Fatalf("unexpected empty values error: got %q want %q", result.Message, want)
|
||||
}
|
||||
if client.listPushKey != "" || client.listPushLeftKey != "" {
|
||||
t.Fatalf("empty values must not be pushed: right=%q left=%q", client.listPushKey, client.listPushLeftKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisListPushRejectsInvalidPosition(t *testing.T) {
|
||||
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
app.configDir = t.TempDir()
|
||||
|
||||
CloseAllRedisClients()
|
||||
client := &capturingRedisClient{}
|
||||
originalNewRedisClientFunc := newRedisClientFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
newRedisClientFunc = originalNewRedisClientFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
CloseAllRedisClients()
|
||||
}()
|
||||
newRedisClientFunc = func() redislib.RedisClient {
|
||||
return client
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
result := app.RedisListPush(connection.ConnectionConfig{
|
||||
Type: "redis",
|
||||
Host: "redis.local",
|
||||
Port: 6379,
|
||||
}, "tasks", RedisListPushOptions{
|
||||
Values: []string{"review"},
|
||||
Position: "LEFT",
|
||||
})
|
||||
if result.Success {
|
||||
t.Fatalf("RedisListPush should reject an invalid position: %+v", result)
|
||||
}
|
||||
want := app.appText("redis.backend.error.list_position_invalid", nil)
|
||||
if result.Message != want {
|
||||
t.Fatalf("unexpected invalid position error: got %q want %q", result.Message, want)
|
||||
}
|
||||
if client.listPushKey != "" || client.listPushLeftKey != "" {
|
||||
t.Fatalf("invalid position must not be pushed: right=%q left=%q", client.listPushKey, client.listPushLeftKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisListRemoveDeletesTheSelectedValue(t *testing.T) {
|
||||
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
app.configDir = t.TempDir()
|
||||
|
||||
@@ -943,12 +1114,48 @@ func TestRedisListRemoveDeletesOneValue(t *testing.T) {
|
||||
Type: "redis",
|
||||
Host: "redis.local",
|
||||
Port: 6379,
|
||||
}, "tasks", "review")
|
||||
}, "tasks", 3, "review")
|
||||
if !result.Success {
|
||||
t.Fatalf("RedisListRemove returned failure: %+v", result)
|
||||
}
|
||||
if client.removedListKey != "tasks" || client.removedListValue != "review" {
|
||||
t.Fatalf("unexpected list remove call: key=%q value=%q", client.removedListKey, client.removedListValue)
|
||||
if client.removedListKey != "tasks" || client.removedListIndex != 3 || client.removedListValue != "review" {
|
||||
t.Fatalf("unexpected list remove call: key=%q index=%d value=%q", client.removedListKey, client.removedListIndex, client.removedListValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisListRemoveReportsWhenTheSelectedItemChanged(t *testing.T) {
|
||||
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
app.configDir = t.TempDir()
|
||||
|
||||
CloseAllRedisClients()
|
||||
client := &capturingRedisClient{
|
||||
listRemoveErr: fmt.Errorf("concurrent update: %w", redislib.ErrRedisListItemChanged),
|
||||
}
|
||||
originalNewRedisClientFunc := newRedisClientFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
newRedisClientFunc = originalNewRedisClientFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
CloseAllRedisClients()
|
||||
}()
|
||||
newRedisClientFunc = func() redislib.RedisClient {
|
||||
return client
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
result := app.RedisListRemove(connection.ConnectionConfig{
|
||||
Type: "redis",
|
||||
Host: "redis.local",
|
||||
Port: 6379,
|
||||
}, "tasks", 3, "review")
|
||||
if result.Success {
|
||||
t.Fatalf("RedisListRemove should report a changed item: %+v", result)
|
||||
}
|
||||
want := app.appText("redis.backend.error.list_item_changed", nil)
|
||||
if result.Message != want {
|
||||
t.Fatalf("unexpected changed item error: got %q want %q", result.Message, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,9 @@ type RedisClient interface {
|
||||
// List operations
|
||||
GetList(key string, start, stop int64) ([]string, error)
|
||||
ListPush(key string, values ...string) error
|
||||
ListPushLeft(key string, values ...string) error
|
||||
ListSet(key string, index int64, value string) error
|
||||
ListRemove(key, value string) error
|
||||
ListRemoveAt(key string, index int64, expectedValue string) error
|
||||
|
||||
// Set operations
|
||||
GetSet(key string) ([]string, error)
|
||||
|
||||
@@ -19,10 +19,14 @@ import (
|
||||
"GoNavi-Wails/internal/logger"
|
||||
"GoNavi-Wails/internal/ssh"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var ErrRedisKeyGone = errors.New("Redis Key 不存在或已过期")
|
||||
var (
|
||||
ErrRedisKeyGone = errors.New("Redis Key 不存在或已过期")
|
||||
ErrRedisListItemChanged = errors.New("Redis 列表项已变化,请刷新后重试")
|
||||
)
|
||||
|
||||
// RedisClientImpl implements RedisClient using go-redis
|
||||
type RedisClientImpl struct {
|
||||
@@ -49,6 +53,29 @@ const (
|
||||
redisSearchMaxStepCount int64 = 1000
|
||||
redisSearchMaxResultCount = 10000
|
||||
redisSearchMaxDuration = 3 * time.Second
|
||||
redisListRemoveAtScript = `
|
||||
local current = redis.call("LINDEX", KEYS[1], ARGV[1])
|
||||
if current == false or current ~= ARGV[2] then
|
||||
return 0
|
||||
end
|
||||
redis.call("LSET", KEYS[1], ARGV[1], ARGV[3])
|
||||
local removed = redis.pcall("LREM", KEYS[1], 1, ARGV[3])
|
||||
if type(removed) == "table" and removed.err then
|
||||
local restored = redis.pcall("LSET", KEYS[1], ARGV[1], ARGV[2])
|
||||
if type(restored) == "table" and restored.err then
|
||||
return redis.error_reply(removed.err .. "; rollback failed: " .. restored.err)
|
||||
end
|
||||
return redis.error_reply(removed.err)
|
||||
end
|
||||
if removed ~= 1 then
|
||||
local restored = redis.pcall("LSET", KEYS[1], ARGV[1], ARGV[2])
|
||||
if type(restored) == "table" and restored.err then
|
||||
return redis.error_reply("Redis list delete rollback failed: " .. restored.err)
|
||||
end
|
||||
return -1
|
||||
end
|
||||
return 1
|
||||
`
|
||||
)
|
||||
|
||||
var redisDBSwitchConnect = func(client *RedisClientImpl, config connection.ConnectionConfig) error {
|
||||
@@ -1212,6 +1239,20 @@ func (r *RedisClientImpl) ListPush(key string, values ...string) error {
|
||||
return r.client.RPush(ctx, r.toPhysicalKey(key), args...).Err()
|
||||
}
|
||||
|
||||
// ListPushLeft pushes values to the start of a list.
|
||||
func (r *RedisClientImpl) ListPushLeft(key string, values ...string) error {
|
||||
if r.client == nil {
|
||||
return fmt.Errorf("Redis 客户端未连接")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
args := make([]interface{}, len(values))
|
||||
for i, value := range values {
|
||||
args[i] = value
|
||||
}
|
||||
return r.client.LPush(ctx, r.toPhysicalKey(key), args...).Err()
|
||||
}
|
||||
|
||||
// ListSet sets the value at an index in a list
|
||||
func (r *RedisClientImpl) ListSet(key string, index int64, value string) error {
|
||||
if r.client == nil {
|
||||
@@ -1222,14 +1263,39 @@ func (r *RedisClientImpl) ListSet(key string, index int64, value string) error {
|
||||
return r.client.LSet(ctx, r.toPhysicalKey(key), index, value).Err()
|
||||
}
|
||||
|
||||
// ListRemove removes one matching value from a list.
|
||||
func (r *RedisClientImpl) ListRemove(key, value string) error {
|
||||
// ListRemoveAt removes the expected value at an index atomically.
|
||||
// The operation requires Redis scripting permission in addition to list command permissions.
|
||||
func (r *RedisClientImpl) ListRemoveAt(key string, index int64, expectedValue string) error {
|
||||
if r.client == nil {
|
||||
return fmt.Errorf("Redis 客户端未连接")
|
||||
}
|
||||
markerID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成 Redis 列表删除标记失败: %w", err)
|
||||
}
|
||||
marker := "\x00gonavi:list-remove:" + markerID.String()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return r.client.LRem(ctx, r.toPhysicalKey(key), 1, value).Err()
|
||||
result, err := r.client.Eval(
|
||||
ctx,
|
||||
redisListRemoveAtScript,
|
||||
[]string{r.toPhysicalKey(key)},
|
||||
strconv.FormatInt(index, 10),
|
||||
expectedValue,
|
||||
marker,
|
||||
).Int64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch result {
|
||||
case 0:
|
||||
return ErrRedisListItemChanged
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("Redis 列表按索引删除返回异常结果: %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSet gets all members of a set
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func startRedisProtocolTestServer(t *testing.T, handler func([]string) string) string {
|
||||
@@ -576,7 +576,6 @@ func TestRedisConnectFailureWrappersUseEnglishPrefixes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestRedisExecuteCommandClusterSelectValidationUsesEnglishMessages(t *testing.T) {
|
||||
SetBackendLanguage(i18n.LanguageEnUS)
|
||||
t.Cleanup(func() {
|
||||
@@ -634,7 +633,6 @@ func TestRedisExecuteCommandClusterSelectValidationUsesEnglishMessages(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestRedisSelectDBClusterRangeUsesEnglishMessage(t *testing.T) {
|
||||
SetBackendLanguage(i18n.LanguageEnUS)
|
||||
t.Cleanup(func() {
|
||||
@@ -704,17 +702,17 @@ func TestRedisGetDatabasesUsesConfiguredDatabaseCountAboveDefault(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRemoveUsesLRemForOneMatchingValue(t *testing.T) {
|
||||
var commands [][]string
|
||||
func TestListPushUsesRPushWithAllValues(t *testing.T) {
|
||||
commandCh := make(chan []string, 1)
|
||||
addr := startRedisProtocolTestServer(t, func(args []string) string {
|
||||
commands = append(commands, append([]string(nil), args...))
|
||||
switch strings.ToUpper(strings.TrimSpace(args[0])) {
|
||||
case "HELLO":
|
||||
return "-ERR unknown command 'HELLO'\r\n"
|
||||
case "CLIENT":
|
||||
return "-ERR unknown subcommand\r\n"
|
||||
case "LREM":
|
||||
return ":1\r\n"
|
||||
case "RPUSH":
|
||||
commandCh <- append([]string(nil), args...)
|
||||
return ":2\r\n"
|
||||
}
|
||||
return "+OK\r\n"
|
||||
})
|
||||
@@ -729,19 +727,178 @@ func TestListRemoveUsesLRemForOneMatchingValue(t *testing.T) {
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := client.ListRemove("tasks", "review"); err != nil {
|
||||
t.Fatalf("ListRemove returned error: %v", err)
|
||||
if err := client.ListPush("tasks", "review", "ship"); err != nil {
|
||||
t.Fatalf("ListPush returned error: %v", err)
|
||||
}
|
||||
|
||||
for _, command := range commands {
|
||||
if len(command) == 4 && strings.EqualFold(command[0], "LREM") {
|
||||
if command[1] != "tasks" || command[2] != "1" || command[3] != "review" {
|
||||
t.Fatalf("unexpected LREM command: %v", command)
|
||||
}
|
||||
return
|
||||
select {
|
||||
case command := <-commandCh:
|
||||
if len(command) != 4 || command[1] != "tasks" || command[2] != "review" || command[3] != "ship" {
|
||||
t.Fatalf("unexpected RPUSH command: %v", command)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("expected RPUSH command")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPushLeftUsesLPushWithAllValues(t *testing.T) {
|
||||
commandCh := make(chan []string, 1)
|
||||
addr := startRedisProtocolTestServer(t, func(args []string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(args[0])) {
|
||||
case "HELLO":
|
||||
return "-ERR unknown command 'HELLO'\r\n"
|
||||
case "CLIENT":
|
||||
return "-ERR unknown subcommand\r\n"
|
||||
case "LPUSH":
|
||||
commandCh <- append([]string(nil), args...)
|
||||
return ":2\r\n"
|
||||
}
|
||||
return "+OK\r\n"
|
||||
})
|
||||
|
||||
rawClient := goredis.NewClient(&goredis.Options{
|
||||
Addr: addr,
|
||||
Protocol: 2,
|
||||
})
|
||||
client := &RedisClientImpl{
|
||||
client: rawClient,
|
||||
singleClient: rawClient,
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := client.ListPushLeft("tasks", "review", "ship"); err != nil {
|
||||
t.Fatalf("ListPushLeft returned error: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case command := <-commandCh:
|
||||
if len(command) != 4 || command[1] != "tasks" || command[2] != "review" || command[3] != "ship" {
|
||||
t.Fatalf("unexpected LPUSH command: %v", command)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("expected LPUSH command")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRemoveAtUsesAtomicEvalWithPhysicalKeyAndExpectedValue(t *testing.T) {
|
||||
commandCh := make(chan []string, 1)
|
||||
addr := startRedisProtocolTestServer(t, func(args []string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(args[0])) {
|
||||
case "HELLO":
|
||||
return "-ERR unknown command 'HELLO'\r\n"
|
||||
case "CLIENT":
|
||||
return "-ERR unknown subcommand\r\n"
|
||||
case "EVAL":
|
||||
commandCh <- append([]string(nil), args...)
|
||||
return ":1\r\n"
|
||||
}
|
||||
return "+OK\r\n"
|
||||
})
|
||||
|
||||
rawClient := goredis.NewClient(&goredis.Options{
|
||||
Addr: addr,
|
||||
Protocol: 2,
|
||||
})
|
||||
client := &RedisClientImpl{
|
||||
client: rawClient,
|
||||
singleClient: rawClient,
|
||||
isCluster: true,
|
||||
currentDB: 3,
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := client.ListRemoveAt("tasks", 3, "review"); err != nil {
|
||||
t.Fatalf("ListRemoveAt returned error: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case command := <-commandCh:
|
||||
if len(command) != 7 {
|
||||
t.Fatalf("unexpected EVAL command length: %v", command)
|
||||
}
|
||||
if command[2] != "1" || command[3] != "__gonavi_db_3__:tasks" {
|
||||
t.Fatalf("expected one physical Redis key, got %v", command)
|
||||
}
|
||||
if command[4] != "3" || command[5] != "review" {
|
||||
t.Fatalf("expected index and value arguments, got %v", command)
|
||||
}
|
||||
if !strings.HasPrefix(command[6], "\x00gonavi:list-remove:") {
|
||||
t.Fatalf("expected collision-resistant marker prefix, got %q", command[6])
|
||||
}
|
||||
for _, redisCommand := range []string{"LINDEX", "LSET", "LREM"} {
|
||||
if !strings.Contains(strings.ToUpper(command[1]), redisCommand) {
|
||||
t.Fatalf("expected script to contain %s, got %q", redisCommand, command[1])
|
||||
}
|
||||
}
|
||||
if !strings.Contains(command[1], `redis.pcall("LREM"`) ||
|
||||
!strings.Contains(command[1], `redis.pcall("LSET"`) ||
|
||||
!strings.Contains(command[1], "rollback failed") {
|
||||
t.Fatalf("expected script to restore the selected item after a removal error, got %q", command[1])
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("expected EVAL command")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRemoveAtReturnsItemChangedWhenIndexNoLongerMatches(t *testing.T) {
|
||||
addr := startRedisProtocolTestServer(t, func(args []string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(args[0])) {
|
||||
case "HELLO":
|
||||
return "-ERR unknown command 'HELLO'\r\n"
|
||||
case "CLIENT":
|
||||
return "-ERR unknown subcommand\r\n"
|
||||
case "EVAL":
|
||||
return ":0\r\n"
|
||||
}
|
||||
return "+OK\r\n"
|
||||
})
|
||||
|
||||
rawClient := goredis.NewClient(&goredis.Options{
|
||||
Addr: addr,
|
||||
Protocol: 2,
|
||||
})
|
||||
client := &RedisClientImpl{
|
||||
client: rawClient,
|
||||
singleClient: rawClient,
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
err := client.ListRemoveAt("tasks", 3, "review")
|
||||
if !errors.Is(err, ErrRedisListItemChanged) {
|
||||
t.Fatalf("expected ErrRedisListItemChanged, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRemoveAtRejectsUnexpectedScriptResult(t *testing.T) {
|
||||
addr := startRedisProtocolTestServer(t, func(args []string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(args[0])) {
|
||||
case "HELLO":
|
||||
return "-ERR unknown command 'HELLO'\r\n"
|
||||
case "CLIENT":
|
||||
return "-ERR unknown subcommand\r\n"
|
||||
case "EVAL":
|
||||
return ":2\r\n"
|
||||
}
|
||||
return "+OK\r\n"
|
||||
})
|
||||
|
||||
rawClient := goredis.NewClient(&goredis.Options{
|
||||
Addr: addr,
|
||||
Protocol: 2,
|
||||
})
|
||||
client := &RedisClientImpl{
|
||||
client: rawClient,
|
||||
singleClient: rawClient,
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
err := client.ListRemoveAt("tasks", 3, "review")
|
||||
if err == nil {
|
||||
t.Fatal("expected unexpected script result to fail")
|
||||
}
|
||||
if errors.Is(err, ErrRedisListItemChanged) {
|
||||
t.Fatalf("expected protocol error, got item-changed sentinel: %v", err)
|
||||
}
|
||||
t.Fatalf("expected LREM command, got %v", commands)
|
||||
}
|
||||
|
||||
func TestRedisSearchScanContinuesPastEmptyMatchedPages(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user