forked from mirror/ledisdb
support sort command
This commit is contained in:
parent
adbfd4b945
commit
b5c78640aa
|
@ -0,0 +1,232 @@
|
|||
package ledis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/siddontang/ledisdb/store"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Limit struct {
|
||||
Offset int
|
||||
Size int
|
||||
}
|
||||
|
||||
func getSortRange(values [][]byte, offset int, size int) (int, int) {
|
||||
var start = 0
|
||||
if offset > 0 {
|
||||
start = offset
|
||||
}
|
||||
|
||||
valueLen := len(values)
|
||||
var end = valueLen - 1
|
||||
if size > 0 {
|
||||
end = start + size - 1
|
||||
}
|
||||
|
||||
if start >= valueLen {
|
||||
start = valueLen - 1
|
||||
end = valueLen - 2
|
||||
}
|
||||
|
||||
if end >= valueLen {
|
||||
end = valueLen - 1
|
||||
}
|
||||
|
||||
return start, end
|
||||
}
|
||||
|
||||
var hashPattern = []byte("*->")
|
||||
|
||||
func (db *DB) lookupKeyByPattern(pattern []byte, subKey []byte) []byte {
|
||||
// If the pattern is #, return the substitution key itself
|
||||
if bytes.Equal(pattern, []byte{'#'}) {
|
||||
return subKey
|
||||
}
|
||||
|
||||
// If we can't find '*' in the pattern, return nil
|
||||
if !bytes.Contains(pattern, []byte{'*'}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := pattern
|
||||
var field []byte = nil
|
||||
|
||||
// Find out if we're dealing with a hash dereference
|
||||
if n := bytes.Index(pattern, hashPattern); n > 0 && n+3 < len(pattern) {
|
||||
key = pattern[0 : n+1]
|
||||
field = pattern[n+3:]
|
||||
}
|
||||
|
||||
// Perform the '*' substitution
|
||||
key = bytes.Replace(key, []byte{'*'}, subKey, 1)
|
||||
|
||||
var value []byte
|
||||
if field == nil {
|
||||
value, _ = db.Get(key)
|
||||
} else {
|
||||
value, _ = db.HGet(key, field)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
type sortItem struct {
|
||||
value []byte
|
||||
cmpValue []byte
|
||||
score float64
|
||||
}
|
||||
|
||||
type sortItemSlice struct {
|
||||
alpha bool
|
||||
sortByPattern bool
|
||||
items []sortItem
|
||||
}
|
||||
|
||||
func (s *sortItemSlice) Len() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
func (s *sortItemSlice) Swap(i, j int) {
|
||||
s.items[i], s.items[j] = s.items[j], s.items[i]
|
||||
}
|
||||
|
||||
func (s *sortItemSlice) Less(i, j int) bool {
|
||||
s1 := s.items[i]
|
||||
s2 := s.items[j]
|
||||
if !s.alpha {
|
||||
if s1.score < s2.score {
|
||||
return true
|
||||
} else if s1.score > s2.score {
|
||||
return false
|
||||
} else {
|
||||
return bytes.Compare(s1.value, s2.value) < 0
|
||||
}
|
||||
} else {
|
||||
if s.sortByPattern {
|
||||
if s1.cmpValue == nil || s2.cmpValue == nil {
|
||||
if s1.cmpValue == nil {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// Unlike redis, we only use bytes compare
|
||||
return bytes.Compare(s1.cmpValue, s2.cmpValue) < 0
|
||||
}
|
||||
} else {
|
||||
// Unlike redis, we only use bytes compare
|
||||
return bytes.Compare(s1.value, s2.value) < 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DB) xsort(values [][]byte, offset int, size int, alpha bool, desc bool, sortBy []byte, sortGet [][]byte) ([][]byte, error) {
|
||||
if len(values) == 0 {
|
||||
return [][]byte{}, nil
|
||||
}
|
||||
|
||||
start, end := getSortRange(values, offset, size)
|
||||
|
||||
dontsort := 0
|
||||
|
||||
if sortBy != nil {
|
||||
if !bytes.Contains(sortBy, []byte{'*'}) {
|
||||
dontsort = 1
|
||||
}
|
||||
}
|
||||
|
||||
items := &sortItemSlice{
|
||||
alpha: alpha,
|
||||
sortByPattern: sortBy != nil,
|
||||
items: make([]sortItem, len(values)),
|
||||
}
|
||||
|
||||
for i, value := range values {
|
||||
items.items[i].value = value
|
||||
items.items[i].score = 0
|
||||
items.items[i].cmpValue = nil
|
||||
|
||||
if dontsort == 0 {
|
||||
var cmpValue []byte
|
||||
if sortBy != nil {
|
||||
cmpValue = db.lookupKeyByPattern(sortBy, value)
|
||||
} else {
|
||||
// use value iteself to sort by
|
||||
cmpValue = value
|
||||
}
|
||||
|
||||
if cmpValue == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if alpha {
|
||||
if sortBy != nil {
|
||||
items.items[i].cmpValue = cmpValue
|
||||
}
|
||||
} else {
|
||||
score, err := strconv.ParseFloat(string(cmpValue), 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s scores can't be converted into double", cmpValue)
|
||||
}
|
||||
items.items[i].score = score
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dontsort == 0 {
|
||||
if !desc {
|
||||
sort.Sort(items)
|
||||
} else {
|
||||
sort.Sort(sort.Reverse(items))
|
||||
}
|
||||
}
|
||||
|
||||
var resLen int = end - start + 1
|
||||
if len(sortGet) > 0 {
|
||||
resLen = len(sortGet) * (end - start + 1)
|
||||
}
|
||||
|
||||
res := make([][]byte, 0, resLen)
|
||||
for i := start; i <= end; i++ {
|
||||
if len(sortGet) == 0 {
|
||||
res = append(res, items.items[i].value)
|
||||
} else {
|
||||
for _, getPattern := range sortGet {
|
||||
v := db.lookupKeyByPattern(getPattern, items.items[i].value)
|
||||
res = append(res, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (db *DB) XLSort(key []byte, offset int, size int, alpha bool, desc bool, sortBy []byte, sortGet [][]byte) ([][]byte, error) {
|
||||
values, err := db.LRange(key, 0, -1)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.xsort(values, offset, size, alpha, desc, sortBy, sortGet)
|
||||
}
|
||||
|
||||
func (db *DB) XSSort(key []byte, offset int, size int, alpha bool, desc bool, sortBy []byte, sortGet [][]byte) ([][]byte, error) {
|
||||
values, err := db.SMembers(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.xsort(values, offset, size, alpha, desc, sortBy, sortGet)
|
||||
}
|
||||
|
||||
func (db *DB) XZSort(key []byte, offset int, size int, alpha bool, desc bool, sortBy []byte, sortGet [][]byte) ([][]byte, error) {
|
||||
values, err := db.ZRangeByLex(key, nil, nil, store.RangeClose, 0, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.xsort(values, offset, size, alpha, desc, sortBy, sortGet)
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package ledis
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testBuildValues(values ...string) [][]byte {
|
||||
v := make([][]byte, 0, len(values))
|
||||
for _, value := range values {
|
||||
v = append(v, []byte(value))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func checkSortRes(t *testing.T, res [][]byte, values ...string) {
|
||||
if len(res) != len(values) {
|
||||
debug.PrintStack()
|
||||
t.Fatalf("invalid xsort res len, %d = %d", len(res), len(values))
|
||||
}
|
||||
|
||||
for i := 0; i < len(res); i++ {
|
||||
if string(res[i]) != values[i] {
|
||||
debug.PrintStack()
|
||||
t.Fatalf("invalid xsort res at %d, %s != %s", i, res[i], values[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkTestSort(t *testing.T, db *DB, items []string, offset int, size int, alpha bool,
|
||||
desc bool, sortBy []byte, sortGet [][]byte, checkValues []string) {
|
||||
|
||||
vv := testBuildValues(items...)
|
||||
|
||||
res, err := db.xsort(vv, offset, size, alpha, desc, sortBy, sortGet)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkSortRes(t, res, checkValues...)
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
db := getTestDB()
|
||||
|
||||
db.FlushAll()
|
||||
|
||||
// Prepare data
|
||||
db.MSet(
|
||||
KVPair{[]byte("weight_1"), []byte("30")},
|
||||
KVPair{[]byte("weight_2"), []byte("20")},
|
||||
KVPair{[]byte("weight_3"), []byte("10")},
|
||||
KVPair{[]byte("weight_a"), []byte("60")},
|
||||
KVPair{[]byte("weight_b"), []byte("50")},
|
||||
KVPair{[]byte("weight_c"), []byte("40")})
|
||||
|
||||
db.HSet([]byte("hash_weight_1"), []byte("index"), []byte("30"))
|
||||
db.HSet([]byte("hash_weight_2"), []byte("index"), []byte("20"))
|
||||
db.HSet([]byte("hash_weight_3"), []byte("index"), []byte("10"))
|
||||
db.HSet([]byte("hash_weight_a"), []byte("index"), []byte("60"))
|
||||
db.HSet([]byte("hash_weight_b"), []byte("index"), []byte("50"))
|
||||
db.HSet([]byte("hash_weight_c"), []byte("index"), []byte("40"))
|
||||
|
||||
db.MSet(
|
||||
KVPair{[]byte("object_1"), []byte("30")},
|
||||
KVPair{[]byte("object_2"), []byte("20")},
|
||||
KVPair{[]byte("object_3"), []byte("10")},
|
||||
KVPair{[]byte("number_1"), []byte("10")},
|
||||
KVPair{[]byte("number_2"), []byte("20")},
|
||||
KVPair{[]byte("number_3"), []byte("30")},
|
||||
KVPair{[]byte("object_a"), []byte("60")},
|
||||
KVPair{[]byte("object_b"), []byte("50")},
|
||||
KVPair{[]byte("object_c"), []byte("40")},
|
||||
KVPair{[]byte("number_a"), []byte("40")},
|
||||
KVPair{[]byte("number_b"), []byte("50")},
|
||||
KVPair{[]byte("number_c"), []byte("60")})
|
||||
|
||||
db.HSet([]byte("hash_object_1"), []byte("index"), []byte("30"))
|
||||
db.HSet([]byte("hash_object_2"), []byte("index"), []byte("20"))
|
||||
db.HSet([]byte("hash_object_3"), []byte("index"), []byte("10"))
|
||||
db.HSet([]byte("hash_number_1"), []byte("index"), []byte("10"))
|
||||
db.HSet([]byte("hash_number_2"), []byte("index"), []byte("20"))
|
||||
db.HSet([]byte("hash_number_3"), []byte("index"), []byte("30"))
|
||||
|
||||
db.HSet([]byte("hash_object_a"), []byte("index"), []byte("60"))
|
||||
db.HSet([]byte("hash_object_b"), []byte("index"), []byte("50"))
|
||||
db.HSet([]byte("hash_object_c"), []byte("index"), []byte("40"))
|
||||
db.HSet([]byte("hash_number_a"), []byte("index"), []byte("40"))
|
||||
db.HSet([]byte("hash_number_b"), []byte("index"), []byte("50"))
|
||||
db.HSet([]byte("hash_number_c"), []byte("index"), []byte("60"))
|
||||
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, false, nil, nil, []string{"1", "2", "3"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, 1, false, false, nil, nil, []string{"1"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, 2, false, false, nil, nil, []string{"1", "2"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, 3, false, false, nil, nil, []string{"1", "2", "3"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, 4, false, false, nil, nil, []string{"1", "2", "3"})
|
||||
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, true, false, nil, nil, []string{"1", "2", "3"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, true, nil, nil, []string{"3", "2", "1"})
|
||||
|
||||
if _, err := db.xsort(testBuildValues("c", "b", "a"), 0, -1, false, false, nil, nil); err == nil {
|
||||
t.Fatal("must nil")
|
||||
}
|
||||
|
||||
checkTestSort(t, db, []string{"c", "b", "a"}, 0, -1, true, false, nil, nil, []string{"a", "b", "c"})
|
||||
checkTestSort(t, db, []string{"c", "b", "a"}, 0, -1, true, true, nil, nil, []string{"c", "b", "a"})
|
||||
|
||||
checkTestSort(t, db, []string{"1", "2", "3"}, 0, 1, false, false, nil, nil, []string{"1"})
|
||||
checkTestSort(t, db, []string{"1", "2", "3"}, 0, 1, false, true, nil, nil, []string{"3"})
|
||||
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, false, []byte("abc"), nil, []string{"3", "2", "1"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, false, []byte("weight_*"), nil, []string{"3", "2", "1"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, true, []byte("weight_*"), nil, []string{"1", "2", "3"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, false, []byte("hash_weight_*->index"), nil, []string{"3", "2", "1"})
|
||||
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, false, nil, [][]byte{[]byte("object_*")}, []string{"30", "20", "10"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, false, nil, [][]byte{[]byte("number_*")}, []string{"10", "20", "30"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, false, nil, [][]byte{[]byte("#"), []byte("number_*")},
|
||||
[]string{"1", "10", "2", "20", "3", "30"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, false, nil, [][]byte{[]byte("object_*"), []byte("number_*")},
|
||||
[]string{"30", "10", "20", "20", "10", "30"})
|
||||
checkTestSort(t, db, []string{"3", "2", "1"}, 0, -1, false, false, nil, [][]byte{[]byte("object_*_abc")}, []string{"", "", ""})
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func xsort(c *client, tp string, key []byte, offset int, size int, alpha bool,
|
||||
desc bool, sortBy []byte, sortGet [][]byte) ([][]byte, error) {
|
||||
var ay [][]byte
|
||||
var err error
|
||||
switch strings.ToUpper(tp) {
|
||||
case "LIST":
|
||||
ay, err = c.db.XLSort(key, offset, size, alpha, desc, sortBy, sortGet)
|
||||
case "SET":
|
||||
ay, err = c.db.XSSort(key, offset, size, alpha, desc, sortBy, sortGet)
|
||||
case "ZSET":
|
||||
ay, err = c.db.XZSort(key, offset, size, alpha, desc, sortBy, sortGet)
|
||||
default:
|
||||
err = fmt.Errorf("invalid key type %s", tp)
|
||||
}
|
||||
return ay, err
|
||||
}
|
||||
|
||||
func xlsortCommand(c *client) error {
|
||||
return handleXSort(c, "LIST")
|
||||
}
|
||||
func xssortCommand(c *client) error {
|
||||
return handleXSort(c, "SET")
|
||||
}
|
||||
func xzsortCommand(c *client) error {
|
||||
return handleXSort(c, "ZSET")
|
||||
}
|
||||
|
||||
var ascArg = []byte("asc")
|
||||
var descArg = []byte("desc")
|
||||
var alphaArg = []byte("alpha")
|
||||
var limitArg = []byte("limit")
|
||||
var storeArg = []byte("store")
|
||||
var byArg = []byte("by")
|
||||
var getArg = []byte("get")
|
||||
|
||||
func handleXSort(c *client, tp string) error {
|
||||
args := c.args
|
||||
if len(args) == 0 {
|
||||
return ErrCmdParams
|
||||
}
|
||||
|
||||
key := args[0]
|
||||
desc := false
|
||||
alpha := false
|
||||
offset := 0
|
||||
size := 0
|
||||
var storeKey []byte
|
||||
var sortBy []byte
|
||||
var sortGet [][]byte
|
||||
var err error
|
||||
|
||||
for i := 1; i < len(args); {
|
||||
if bytes.EqualFold(args[i], ascArg) {
|
||||
desc = false
|
||||
} else if bytes.EqualFold(args[i], descArg) {
|
||||
desc = true
|
||||
} else if bytes.EqualFold(args[i], alphaArg) {
|
||||
alpha = true
|
||||
} else if bytes.EqualFold(args[i], limitArg) && i+2 < len(args) {
|
||||
if offset, err = strconv.Atoi(string(args[i+1])); err != nil {
|
||||
return err
|
||||
}
|
||||
if size, err = strconv.Atoi(string(args[i+2])); err != nil {
|
||||
return err
|
||||
}
|
||||
i = i + 2
|
||||
} else if bytes.EqualFold(args[i], storeArg) && i+1 < len(args) {
|
||||
storeKey = args[i+1]
|
||||
i++
|
||||
} else if bytes.EqualFold(args[i], byArg) && i+1 < len(args) {
|
||||
sortBy = args[i+1]
|
||||
i++
|
||||
} else if bytes.EqualFold(args[i], getArg) && i+1 < len(args) {
|
||||
sortGet = append(sortGet, args[i+1])
|
||||
i++
|
||||
} else {
|
||||
return ErrCmdParams
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
ay, err := xsort(c, tp, key, offset, size, alpha, desc, sortBy, sortGet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if storeKey == nil {
|
||||
c.resp.writeSliceArray(ay)
|
||||
} else {
|
||||
// not threadsafe now, need lock???
|
||||
if _, err = c.db.LClear(storeKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n, err := c.db.RPush(storeKey, ay...); err != nil {
|
||||
return err
|
||||
} else {
|
||||
c.resp.writeInteger(n)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
register("xlsort", xlsortCommand)
|
||||
register("xssort", xssortCommand)
|
||||
register("xzsort", xzsortCommand)
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
goledis "github.com/siddontang/ledisdb/client/go/ledis"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func checkTestSortRes(ay interface{}, checks []string) error {
|
||||
values, ok := ay.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid res type %T, must [][]byte", ay)
|
||||
}
|
||||
|
||||
if len(values) != len(checks) {
|
||||
return fmt.Errorf("invalid res number %d != %d", len(values), len(checks))
|
||||
}
|
||||
|
||||
for i, _ := range values {
|
||||
if string(values[i].([]byte)) != checks[i] {
|
||||
return fmt.Errorf("invalid res at %d, %s != %s", i, values[i], checks[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
c := getTestConn()
|
||||
defer c.Close()
|
||||
|
||||
key := "my_sort_key"
|
||||
storeKey := "my_sort_store_key"
|
||||
|
||||
if _, err := c.Do("LPUSH", key, 3, 2, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.Do("MSET", "weight_1", 3, "weight_2", 2, "weight_3", 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.Do("MSET", "object_1", 10, "object_2", 20, "object_3", 30); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ay, err := c.Do("XLSORT", key); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err = checkTestSortRes(ay, []string{"1", "2", "3"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ay, err := c.Do("XLSORT", key, "DESC"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err = checkTestSortRes(ay, []string{"3", "2", "1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ay, err := c.Do("XLSORT", key, "LIMIT", 0, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err = checkTestSortRes(ay, []string{"1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ay, err := c.Do("XLSORT", key, "BY", "weight_*"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err = checkTestSortRes(ay, []string{"3", "2", "1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ay, err := c.Do("XLSORT", key, "GET", "object_*"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err = checkTestSortRes(ay, []string{"10", "20", "30"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ay, err := c.Do("XLSORT", key, "GET", "object_*", "GET", "#"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err = checkTestSortRes(ay, []string{"10", "1", "20", "2", "30", "3"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if n, err := goledis.Int(c.Do("XLSORT", key, "STORE", storeKey)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n != 3 {
|
||||
t.Fatalf("invalid return store sort number, %d != 3", n)
|
||||
} else if ay, err := c.Do("LRANGE", storeKey, 0, -1); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err = checkTestSortRes(ay, []string{"1", "2", "3"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue