495 lines
11 KiB
Go
495 lines
11 KiB
Go
// Copyright 2012 Gary Burd
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
|
// not use this file except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package redis_test
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/garyburd/redigo/redis"
|
|
)
|
|
|
|
type durationScan struct {
|
|
time.Duration `redis:"sd"`
|
|
}
|
|
|
|
func (t *durationScan) RedisScan(src interface{}) (err error) {
|
|
if t == nil {
|
|
return fmt.Errorf("nil pointer")
|
|
}
|
|
switch src := src.(type) {
|
|
case string:
|
|
t.Duration, err = time.ParseDuration(src)
|
|
case []byte:
|
|
t.Duration, err = time.ParseDuration(string(src))
|
|
case int64:
|
|
t.Duration = time.Duration(src)
|
|
default:
|
|
err = fmt.Errorf("cannot convert from %T to %T", src, t)
|
|
}
|
|
return err
|
|
}
|
|
|
|
var scanConversionTests = []struct {
|
|
src interface{}
|
|
dest interface{}
|
|
}{
|
|
{[]byte("-inf"), math.Inf(-1)},
|
|
{[]byte("+inf"), math.Inf(1)},
|
|
{[]byte("0"), float64(0)},
|
|
{[]byte("3.14159"), float64(3.14159)},
|
|
{[]byte("3.14"), float32(3.14)},
|
|
{[]byte("-100"), int(-100)},
|
|
{[]byte("101"), int(101)},
|
|
{int64(102), int(102)},
|
|
{[]byte("103"), uint(103)},
|
|
{int64(104), uint(104)},
|
|
{[]byte("105"), int8(105)},
|
|
{int64(106), int8(106)},
|
|
{[]byte("107"), uint8(107)},
|
|
{int64(108), uint8(108)},
|
|
{[]byte("0"), false},
|
|
{int64(0), false},
|
|
{[]byte("f"), false},
|
|
{[]byte("1"), true},
|
|
{int64(1), true},
|
|
{[]byte("t"), true},
|
|
{"hello", "hello"},
|
|
{[]byte("hello"), "hello"},
|
|
{[]byte("world"), []byte("world")},
|
|
{[]interface{}{[]byte("foo")}, []interface{}{[]byte("foo")}},
|
|
{[]interface{}{[]byte("foo")}, []string{"foo"}},
|
|
{[]interface{}{[]byte("hello"), []byte("world")}, []string{"hello", "world"}},
|
|
{[]interface{}{[]byte("bar")}, [][]byte{[]byte("bar")}},
|
|
{[]interface{}{[]byte("1")}, []int{1}},
|
|
{[]interface{}{[]byte("1"), []byte("2")}, []int{1, 2}},
|
|
{[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}},
|
|
{[]interface{}{[]byte("1")}, []byte{1}},
|
|
{[]interface{}{[]byte("1")}, []bool{true}},
|
|
{"1m", durationScan{Duration: time.Minute}},
|
|
{[]byte("1m"), durationScan{Duration: time.Minute}},
|
|
{time.Minute.Nanoseconds(), durationScan{Duration: time.Minute}},
|
|
{[]interface{}{[]byte("1m")}, []durationScan{durationScan{Duration: time.Minute}}},
|
|
{[]interface{}{[]byte("1m")}, []*durationScan{&durationScan{Duration: time.Minute}}},
|
|
}
|
|
|
|
func TestScanConversion(t *testing.T) {
|
|
for _, tt := range scanConversionTests {
|
|
values := []interface{}{tt.src}
|
|
dest := reflect.New(reflect.TypeOf(tt.dest))
|
|
values, err := redis.Scan(values, dest.Interface())
|
|
if err != nil {
|
|
t.Errorf("Scan(%v) returned error %v", tt, err)
|
|
continue
|
|
}
|
|
if !reflect.DeepEqual(tt.dest, dest.Elem().Interface()) {
|
|
t.Errorf("Scan(%v) returned %v, want %v", tt, dest.Elem().Interface(), tt.dest)
|
|
}
|
|
}
|
|
}
|
|
|
|
var scanConversionErrorTests = []struct {
|
|
src interface{}
|
|
dest interface{}
|
|
}{
|
|
{[]byte("1234"), byte(0)},
|
|
{int64(1234), byte(0)},
|
|
{[]byte("-1"), byte(0)},
|
|
{int64(-1), byte(0)},
|
|
{[]byte("junk"), false},
|
|
{redis.Error("blah"), false},
|
|
{redis.Error("blah"), durationScan{Duration: time.Minute}},
|
|
{"invalid", durationScan{Duration: time.Minute}},
|
|
}
|
|
|
|
func TestScanConversionError(t *testing.T) {
|
|
for _, tt := range scanConversionErrorTests {
|
|
values := []interface{}{tt.src}
|
|
dest := reflect.New(reflect.TypeOf(tt.dest))
|
|
values, err := redis.Scan(values, dest.Interface())
|
|
if err == nil {
|
|
t.Errorf("Scan(%v) did not return error", tt)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ExampleScan() {
|
|
c, err := dial()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
defer c.Close()
|
|
|
|
c.Send("HMSET", "album:1", "title", "Red", "rating", 5)
|
|
c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1)
|
|
c.Send("HMSET", "album:3", "title", "Beat")
|
|
c.Send("LPUSH", "albums", "1")
|
|
c.Send("LPUSH", "albums", "2")
|
|
c.Send("LPUSH", "albums", "3")
|
|
values, err := redis.Values(c.Do("SORT", "albums",
|
|
"BY", "album:*->rating",
|
|
"GET", "album:*->title",
|
|
"GET", "album:*->rating"))
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
for len(values) > 0 {
|
|
var title string
|
|
rating := -1 // initialize to illegal value to detect nil.
|
|
values, err = redis.Scan(values, &title, &rating)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
if rating == -1 {
|
|
fmt.Println(title, "not-rated")
|
|
} else {
|
|
fmt.Println(title, rating)
|
|
}
|
|
}
|
|
// Output:
|
|
// Beat not-rated
|
|
// Earthbound 1
|
|
// Red 5
|
|
}
|
|
|
|
type s0 struct {
|
|
X int
|
|
Y int `redis:"y"`
|
|
Bt bool
|
|
}
|
|
|
|
type s1 struct {
|
|
X int `redis:"-"`
|
|
I int `redis:"i"`
|
|
U uint `redis:"u"`
|
|
S string `redis:"s"`
|
|
P []byte `redis:"p"`
|
|
B bool `redis:"b"`
|
|
Bt bool
|
|
Bf bool
|
|
s0
|
|
Sd durationScan `redis:"sd"`
|
|
Sdp *durationScan `redis:"sdp"`
|
|
}
|
|
|
|
var scanStructTests = []struct {
|
|
title string
|
|
reply []string
|
|
value interface{}
|
|
}{
|
|
{"basic",
|
|
[]string{
|
|
"i", "-1234",
|
|
"u", "5678",
|
|
"s", "hello",
|
|
"p", "world",
|
|
"b", "t",
|
|
"Bt", "1",
|
|
"Bf", "0",
|
|
"X", "123",
|
|
"y", "456",
|
|
"sd", "1m",
|
|
"sdp", "1m",
|
|
},
|
|
&s1{
|
|
I: -1234,
|
|
U: 5678,
|
|
S: "hello",
|
|
P: []byte("world"),
|
|
B: true,
|
|
Bt: true,
|
|
Bf: false,
|
|
s0: s0{X: 123, Y: 456},
|
|
Sd: durationScan{Duration: time.Minute},
|
|
Sdp: &durationScan{Duration: time.Minute},
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestScanStruct(t *testing.T) {
|
|
for _, tt := range scanStructTests {
|
|
|
|
var reply []interface{}
|
|
for _, v := range tt.reply {
|
|
reply = append(reply, []byte(v))
|
|
}
|
|
|
|
value := reflect.New(reflect.ValueOf(tt.value).Type().Elem())
|
|
|
|
if err := redis.ScanStruct(reply, value.Interface()); err != nil {
|
|
t.Fatalf("ScanStruct(%s) returned error %v", tt.title, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(value.Interface(), tt.value) {
|
|
t.Fatalf("ScanStruct(%s) returned %v, want %v", tt.title, value.Interface(), tt.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBadScanStructArgs(t *testing.T) {
|
|
x := []interface{}{"A", "b"}
|
|
test := func(v interface{}) {
|
|
if err := redis.ScanStruct(x, v); err == nil {
|
|
t.Errorf("Expect error for ScanStruct(%T, %T)", x, v)
|
|
}
|
|
}
|
|
|
|
test(nil)
|
|
|
|
var v0 *struct{}
|
|
test(v0)
|
|
|
|
var v1 int
|
|
test(&v1)
|
|
|
|
x = x[:1]
|
|
v2 := struct{ A string }{}
|
|
test(&v2)
|
|
}
|
|
|
|
var scanSliceTests = []struct {
|
|
src []interface{}
|
|
fieldNames []string
|
|
ok bool
|
|
dest interface{}
|
|
}{
|
|
{
|
|
[]interface{}{[]byte("1"), nil, []byte("-1")},
|
|
nil,
|
|
true,
|
|
[]int{1, 0, -1},
|
|
},
|
|
{
|
|
[]interface{}{[]byte("1"), nil, []byte("2")},
|
|
nil,
|
|
true,
|
|
[]uint{1, 0, 2},
|
|
},
|
|
{
|
|
[]interface{}{[]byte("-1")},
|
|
nil,
|
|
false,
|
|
[]uint{1},
|
|
},
|
|
{
|
|
[]interface{}{[]byte("hello"), nil, []byte("world")},
|
|
nil,
|
|
true,
|
|
[][]byte{[]byte("hello"), nil, []byte("world")},
|
|
},
|
|
{
|
|
[]interface{}{[]byte("hello"), nil, []byte("world")},
|
|
nil,
|
|
true,
|
|
[]string{"hello", "", "world"},
|
|
},
|
|
{
|
|
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
|
|
nil,
|
|
true,
|
|
[]struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}},
|
|
},
|
|
{
|
|
[]interface{}{[]byte("a1"), []byte("b1")},
|
|
nil,
|
|
false,
|
|
[]struct{ A, B, C string }{{"a1", "b1", ""}},
|
|
},
|
|
{
|
|
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
|
|
nil,
|
|
true,
|
|
[]*struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}},
|
|
},
|
|
{
|
|
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
|
|
[]string{"A", "B"},
|
|
true,
|
|
[]struct{ A, C, B string }{{"a1", "", "b1"}, {"a2", "", "b2"}},
|
|
},
|
|
{
|
|
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
|
|
nil,
|
|
false,
|
|
[]struct{}{},
|
|
},
|
|
}
|
|
|
|
func TestScanSlice(t *testing.T) {
|
|
for _, tt := range scanSliceTests {
|
|
|
|
typ := reflect.ValueOf(tt.dest).Type()
|
|
dest := reflect.New(typ)
|
|
|
|
err := redis.ScanSlice(tt.src, dest.Interface(), tt.fieldNames...)
|
|
if tt.ok != (err == nil) {
|
|
t.Errorf("ScanSlice(%v, []%s, %v) returned error %v", tt.src, typ, tt.fieldNames, err)
|
|
continue
|
|
}
|
|
if tt.ok && !reflect.DeepEqual(dest.Elem().Interface(), tt.dest) {
|
|
t.Errorf("ScanSlice(src, []%s) returned %#v, want %#v", typ, dest.Elem().Interface(), tt.dest)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ExampleScanSlice() {
|
|
c, err := dial()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
defer c.Close()
|
|
|
|
c.Send("HMSET", "album:1", "title", "Red", "rating", 5)
|
|
c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1)
|
|
c.Send("HMSET", "album:3", "title", "Beat", "rating", 4)
|
|
c.Send("LPUSH", "albums", "1")
|
|
c.Send("LPUSH", "albums", "2")
|
|
c.Send("LPUSH", "albums", "3")
|
|
values, err := redis.Values(c.Do("SORT", "albums",
|
|
"BY", "album:*->rating",
|
|
"GET", "album:*->title",
|
|
"GET", "album:*->rating"))
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
var albums []struct {
|
|
Title string
|
|
Rating int
|
|
}
|
|
if err := redis.ScanSlice(values, &albums); err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
fmt.Printf("%v\n", albums)
|
|
// Output:
|
|
// [{Earthbound 1} {Beat 4} {Red 5}]
|
|
}
|
|
|
|
var argsTests = []struct {
|
|
title string
|
|
actual redis.Args
|
|
expected redis.Args
|
|
}{
|
|
{"struct ptr",
|
|
redis.Args{}.AddFlat(&struct {
|
|
I int `redis:"i"`
|
|
U uint `redis:"u"`
|
|
S string `redis:"s"`
|
|
P []byte `redis:"p"`
|
|
M map[string]string `redis:"m"`
|
|
Bt bool
|
|
Bf bool
|
|
}{
|
|
-1234, 5678, "hello", []byte("world"), map[string]string{"hello": "world"}, true, false,
|
|
}),
|
|
redis.Args{"i", int(-1234), "u", uint(5678), "s", "hello", "p", []byte("world"), "m", map[string]string{"hello": "world"}, "Bt", true, "Bf", false},
|
|
},
|
|
{"struct",
|
|
redis.Args{}.AddFlat(struct{ I int }{123}),
|
|
redis.Args{"I", 123},
|
|
},
|
|
{"slice",
|
|
redis.Args{}.Add(1).AddFlat([]string{"a", "b", "c"}).Add(2),
|
|
redis.Args{1, "a", "b", "c", 2},
|
|
},
|
|
{"struct omitempty",
|
|
redis.Args{}.AddFlat(&struct {
|
|
I int `redis:"i,omitempty"`
|
|
U uint `redis:"u,omitempty"`
|
|
S string `redis:"s,omitempty"`
|
|
P []byte `redis:"p,omitempty"`
|
|
M map[string]string `redis:"m,omitempty"`
|
|
Bt bool `redis:"Bt,omitempty"`
|
|
Bf bool `redis:"Bf,omitempty"`
|
|
}{
|
|
0, 0, "", []byte{}, map[string]string{}, true, false,
|
|
}),
|
|
redis.Args{"Bt", true},
|
|
},
|
|
}
|
|
|
|
func TestArgs(t *testing.T) {
|
|
for _, tt := range argsTests {
|
|
if !reflect.DeepEqual(tt.actual, tt.expected) {
|
|
t.Fatalf("%s is %v, want %v", tt.title, tt.actual, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ExampleArgs() {
|
|
c, err := dial()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
defer c.Close()
|
|
|
|
var p1, p2 struct {
|
|
Title string `redis:"title"`
|
|
Author string `redis:"author"`
|
|
Body string `redis:"body"`
|
|
}
|
|
|
|
p1.Title = "Example"
|
|
p1.Author = "Gary"
|
|
p1.Body = "Hello"
|
|
|
|
if _, err := c.Do("HMSET", redis.Args{}.Add("id1").AddFlat(&p1)...); err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
m := map[string]string{
|
|
"title": "Example2",
|
|
"author": "Steve",
|
|
"body": "Map",
|
|
}
|
|
|
|
if _, err := c.Do("HMSET", redis.Args{}.Add("id2").AddFlat(m)...); err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
for _, id := range []string{"id1", "id2"} {
|
|
|
|
v, err := redis.Values(c.Do("HGETALL", id))
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
if err := redis.ScanStruct(v, &p2); err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%+v\n", p2)
|
|
}
|
|
|
|
// Output:
|
|
// {Title:Example Author:Gary Body:Hello}
|
|
// {Title:Example2 Author:Steve Body:Map}
|
|
}
|