Files
opencloud/pkg/config/envdecode/envdecode_test.go
Jörn Friedrich Dreyer b07b5a1149 use plain pkg module
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2025-01-13 16:42:19 +01:00

791 lines
20 KiB
Go

package envdecode
import (
"encoding/json"
"fmt"
"math"
"net/url"
"os"
"reflect"
"sort"
"strconv"
"testing"
"time"
)
type nested struct {
String string `env:"TEST_STRING"`
}
type testConfig struct {
String string `env:"TEST_STRING"`
Int64 int64 `env:"TEST_INT64"`
Uint16 uint16 `env:"TEST_UINT16"`
Float64 float64 `env:"TEST_FLOAT64"`
Bool bool `env:"TEST_BOOL"`
Duration time.Duration `env:"TEST_DURATION"`
URL *url.URL `env:"TEST_URL"`
StringSlice []string `env:"TEST_STRING_SLICE"`
Int64Slice []int64 `env:"TEST_INT64_SLICE"`
Uint16Slice []uint16 `env:"TEST_UINT16_SLICE"`
Float64Slice []float64 `env:"TEST_FLOAT64_SLICE"`
BoolSlice []bool `env:"TEST_BOOL_SLICE"`
DurationSlice []time.Duration `env:"TEST_DURATION_SLICE"`
URLSlice []*url.URL `env:"TEST_URL_SLICE"`
UnsetString string `env:"TEST_UNSET_STRING"`
UnsetInt64 int64 `env:"TEST_UNSET_INT64"`
UnsetDuration time.Duration `env:"TEST_UNSET_DURATION"`
UnsetURL *url.URL `env:"TEST_UNSET_URL"`
UnsetSlice []string `env:"TEST_UNSET_SLICE"`
InvalidInt64 int64 `env:"TEST_INVALID_INT64"`
UnusedField string
unexportedField string
IgnoredPtr *bool `env:"TEST_BOOL"`
Nested nested
NestedPtr *nested
DecoderStruct decoderStruct `env:"TEST_DECODER_STRUCT"`
DecoderStructPtr *decoderStruct `env:"TEST_DECODER_STRUCT_PTR"`
DecoderString decoderString `env:"TEST_DECODER_STRING"`
UnmarshalerNumber unmarshalerNumber `env:"TEST_UNMARSHALER_NUMBER"`
DefaultInt int `env:"TEST_UNSET,asdf=asdf,default=1234"`
DefaultSliceInt []int `env:"TEST_UNSET,asdf=asdf,default=1"`
DefaultDuration time.Duration `env:"TEST_UNSET,asdf=asdf,default=24h"`
DefaultURL *url.URL `env:"TEST_UNSET,default=http://example.com"`
}
type testConfigNoSet struct {
Some string `env:"TEST_THIS_ENV_WILL_NOT_BE_SET"`
}
type testConfigRequired struct {
Required string `env:"TEST_REQUIRED,required"`
}
type testConfigRequiredDefault struct {
RequiredDefault string `env:"TEST_REQUIRED_DEFAULT,required,default=test"`
}
type testConfigOverride struct {
OverrideString string `env:"TEST_OVERRIDE_A;TEST_OVERRIDE_B,default=override_default"`
}
type testNoExportedFields struct {
// following unexported fields are used for tests
aString string `env:"TEST_STRING"` //nolint:structcheck,unused
anInt64 int64 `env:"TEST_INT64"` //nolint:structcheck,unused
aUint16 uint16 `env:"TEST_UINT16"` //nolint:structcheck,unused
aFloat64 float64 `env:"TEST_FLOAT64"` //nolint:structcheck,unused
aBool bool `env:"TEST_BOOL"` //nolint:structcheck,unused
}
type testNoTags struct {
String string
}
type decoderStruct struct {
String string
}
func (d *decoderStruct) Decode(env string) error {
return json.Unmarshal([]byte(env), &d)
}
type decoderString string
func (d *decoderString) Decode(env string) error {
r, l := []rune(env), len(env)
for i := 0; i < l/2; i++ {
r[i], r[l-1-i] = r[l-1-i], r[i]
}
*d = decoderString(r)
return nil
}
type unmarshalerNumber uint8
func (o *unmarshalerNumber) UnmarshalText(raw []byte) error {
n, err := strconv.ParseUint(string(raw), 8, 8) // parse text as octal number
if err != nil {
return err
}
*o = unmarshalerNumber(n)
return nil
}
func TestDecode(t *testing.T) {
int64Val := int64(-(1 << 50))
int64AsString := fmt.Sprintf("%d", int64Val)
piAsString := fmt.Sprintf("%.48f", math.Pi)
os.Setenv("TEST_STRING", "foo")
os.Setenv("TEST_INT64", int64AsString)
os.Setenv("TEST_UINT16", "60000")
os.Setenv("TEST_FLOAT64", piAsString)
os.Setenv("TEST_BOOL", "true")
os.Setenv("TEST_DURATION", "10m")
os.Setenv("TEST_URL", "https://example.com")
os.Setenv("TEST_INVALID_INT64", "asdf")
os.Setenv("TEST_STRING_SLICE", "foo,bar")
os.Setenv("TEST_INT64_SLICE", int64AsString+","+int64AsString)
os.Setenv("TEST_UINT16_SLICE", "60000,50000")
os.Setenv("TEST_FLOAT64_SLICE", piAsString+","+piAsString)
os.Setenv("TEST_BOOL_SLICE", "true, false, true")
os.Setenv("TEST_DURATION_SLICE", "10m, 20m")
os.Setenv("TEST_URL_SLICE", "https://example.com")
os.Setenv("TEST_DECODER_STRUCT", "{\"string\":\"foo\"}")
os.Setenv("TEST_DECODER_STRUCT_PTR", "{\"string\":\"foo\"}")
os.Setenv("TEST_DECODER_STRING", "oof")
os.Setenv("TEST_UNMARSHALER_NUMBER", "07")
var tc testConfig
tc.NestedPtr = &nested{}
tc.DecoderStructPtr = &decoderStruct{}
err := Decode(&tc)
if err != nil {
t.Fatal(err)
}
if tc.String != "foo" {
t.Fatalf(`Expected "foo", got "%s"`, tc.String)
}
if tc.Int64 != -(1 << 50) {
t.Fatalf("Expected %d, got %d", -(1 << 50), tc.Int64)
}
if tc.Uint16 != 60000 {
t.Fatalf("Expected 60000, got %d", tc.Uint16)
}
if tc.Float64 != math.Pi {
t.Fatalf("Expected %.48f, got %.48f", math.Pi, tc.Float64)
}
if !tc.Bool {
t.Fatal("Expected true, got false")
}
duration, _ := time.ParseDuration("10m")
if tc.Duration != duration {
t.Fatalf("Expected %d, got %d", duration, tc.Duration)
}
if tc.URL == nil {
t.Fatalf("Expected https://example.com, got nil")
} else if tc.URL.String() != "https://example.com" {
t.Fatalf("Expected https://example.com, got %s", tc.URL.String())
}
expectedStringSlice := []string{"foo", "bar"}
if !reflect.DeepEqual(tc.StringSlice, expectedStringSlice) {
t.Fatalf("Expected %s, got %s", expectedStringSlice, tc.StringSlice)
}
expectedInt64Slice := []int64{int64Val, int64Val}
if !reflect.DeepEqual(tc.Int64Slice, expectedInt64Slice) {
t.Fatalf("Expected %#v, got %#v", expectedInt64Slice, tc.Int64Slice)
}
expectedUint16Slice := []uint16{60000, 50000}
if !reflect.DeepEqual(tc.Uint16Slice, expectedUint16Slice) {
t.Fatalf("Expected %#v, got %#v", expectedUint16Slice, tc.Uint16Slice)
}
expectedFloat64Slice := []float64{math.Pi, math.Pi}
if !reflect.DeepEqual(tc.Float64Slice, expectedFloat64Slice) {
t.Fatalf("Expected %#v, got %#v", expectedFloat64Slice, tc.Float64Slice)
}
expectedBoolSlice := []bool{true, false, true}
if !reflect.DeepEqual(tc.BoolSlice, expectedBoolSlice) {
t.Fatalf("Expected %#v, got %#v", expectedBoolSlice, tc.BoolSlice)
}
duration2, _ := time.ParseDuration("20m")
expectedDurationSlice := []time.Duration{duration, duration2}
if !reflect.DeepEqual(tc.DurationSlice, expectedDurationSlice) {
t.Fatalf("Expected %s, got %s", expectedDurationSlice, tc.DurationSlice)
}
urlVal, _ := url.Parse("https://example.com")
expectedURLSlice := []*url.URL{urlVal}
if !reflect.DeepEqual(tc.URLSlice, expectedURLSlice) {
t.Fatalf("Expected %s, got %s", expectedURLSlice, tc.URLSlice)
}
if tc.UnsetString != "" {
t.Fatal("Got non-empty string unexpectedly")
}
if tc.UnsetInt64 != 0 {
t.Fatal("Got non-zero int unexpectedly")
}
if tc.UnsetDuration != time.Duration(0) {
t.Fatal("Got non-zero time.Duration unexpectedly")
}
if tc.UnsetURL != nil {
t.Fatal("Got non-zero *url.URL unexpectedly")
}
if len(tc.UnsetSlice) > 0 {
t.Fatal("Got not-empty string slice unexpectedly")
}
if tc.InvalidInt64 != 0 {
t.Fatal("Got non-zero int unexpectedly")
}
if tc.UnusedField != "" {
t.Fatal("Expected empty field")
}
if tc.unexportedField != "" {
t.Fatal("Expected empty field")
}
if tc.IgnoredPtr != nil {
t.Fatal("Expected nil pointer")
}
if tc.Nested.String != "foo" {
t.Fatalf(`Expected "foo", got "%s"`, tc.Nested.String)
}
if tc.NestedPtr.String != "foo" {
t.Fatalf(`Expected "foo", got "%s"`, tc.NestedPtr.String)
}
if tc.DefaultInt != 1234 {
t.Fatalf("Expected 1234, got %d", tc.DefaultInt)
}
expectedDefaultSlice := []int{1}
if !reflect.DeepEqual(tc.DefaultSliceInt, expectedDefaultSlice) {
t.Fatalf("Expected %d, got %d", expectedDefaultSlice, tc.DefaultSliceInt)
}
defaultDuration, _ := time.ParseDuration("24h")
if tc.DefaultDuration != defaultDuration {
t.Fatalf("Expected %d, got %d", defaultDuration, tc.DefaultInt)
}
if tc.DefaultURL.String() != "http://example.com" {
t.Fatalf("Expected http://example.com, got %s", tc.DefaultURL.String())
}
if tc.DecoderStruct.String != "foo" {
t.Fatalf("Expected foo, got %s", tc.DecoderStruct.String)
}
if tc.DecoderStructPtr.String != "foo" {
t.Fatalf("Expected foo, got %s", tc.DecoderStructPtr.String)
}
if tc.DecoderString != "foo" {
t.Fatalf("Expected foo, got %s", tc.DecoderString)
}
if tc.UnmarshalerNumber != 07 {
t.Fatalf("Expected 07, got %04o", tc.UnmarshalerNumber)
}
os.Setenv("TEST_REQUIRED", "required")
var tcr testConfigRequired
err = Decode(&tcr)
if err != nil {
t.Fatal(err)
}
if tcr.Required != "required" {
t.Fatalf("Expected \"required\", got %s", tcr.Required)
}
_, err = Export(&tcr)
if err != nil {
t.Fatal(err)
}
var tco testConfigOverride
err = Decode(&tco)
if err != nil {
t.Fatal(err)
}
if tco.OverrideString != "override_default" {
t.Fatalf(`Expected "override_default" but got %s`, tco.OverrideString)
}
os.Setenv("TEST_OVERRIDE_A", "override_a")
tco = testConfigOverride{}
err = Decode(&tco)
if err != nil {
t.Fatal(err)
}
if tco.OverrideString != "override_a" {
t.Fatalf(`Expected "override_a" but got %s`, tco.OverrideString)
}
os.Setenv("TEST_OVERRIDE_B", "override_b")
tco = testConfigOverride{}
err = Decode(&tco)
if err != nil {
t.Fatal(err)
}
if tco.OverrideString != "override_b" {
t.Fatalf(`Expected "override_b" but got %s`, tco.OverrideString)
}
}
func TestDecodeErrors(t *testing.T) {
var b bool
err := Decode(&b)
if err != ErrInvalidTarget {
t.Fatal("Should have gotten an error decoding into a bool")
}
var tc testConfig
err = Decode(tc) //nolint:govet
if err != ErrInvalidTarget {
t.Fatal("Should have gotten an error decoding into a non-pointer")
}
var tcp *testConfig
err = Decode(tcp)
if err != ErrInvalidTarget {
t.Fatal("Should have gotten an error decoding to a nil pointer")
}
var tnt testNoTags
err = Decode(&tnt)
if err != ErrNoTargetFieldsAreSet {
t.Fatal("Should have gotten an error decoding a struct with no tags")
}
var tcni testNoExportedFields
err = Decode(&tcni)
if err != ErrNoTargetFieldsAreSet {
t.Fatal("Should have gotten an error decoding a struct with no unexported fields")
}
var tcr testConfigRequired
os.Clearenv()
err = Decode(&tcr)
if err == nil {
t.Fatal("An error was expected but recieved:", err)
}
var tcns testConfigNoSet
err = Decode(&tcns)
if err != ErrNoTargetFieldsAreSet {
t.Fatal("Should have gotten an error decoding when no env variables are set")
}
missing := false
FailureFunc = func(err error) {
missing = true
}
MustDecode(&tcr)
if !missing {
t.Fatal("The FailureFunc should have been called but it was not")
}
var tcrd testConfigRequiredDefault
defer func() {
_ = recover()
}()
_ = Decode(&tcrd)
t.Fatal("This should not have been reached. A panic should have occured.")
}
func TestOnlyNested(t *testing.T) {
os.Setenv("TEST_STRING", "foo")
// No env vars in the outer level are ok, as long as they're
// in the inner struct.
var o struct {
Inner nested
}
if err := Decode(&o); err != nil {
t.Fatalf("Expected no error, got %s", err)
}
// No env vars in the inner levels are ok, as long as they're
// in the outer struct.
var o2 struct {
Inner noConfig
X string `env:"TEST_STRING"`
}
if err := Decode(&o2); err != nil {
t.Fatalf("Expected no error, got %s", err)
}
// No env vars in either outer or inner levels should result
// in error
var o3 struct {
Inner noConfig
}
if err := Decode(&o3); err != ErrNoTargetFieldsAreSet {
t.Fatalf("Expected ErrInvalidTarget, got %s", err)
}
}
func ExampleDecode() {
type Example struct {
// A string field, without any default
String string `env:"EXAMPLE_STRING"`
// A uint16 field, with a default value of 100
Uint16 uint16 `env:"EXAMPLE_UINT16,default=100"`
}
os.Setenv("EXAMPLE_STRING", "an example!")
var e Example
if err := Decode(&e); err != nil {
panic(err)
}
// If TEST_STRING is set, e.String will contain its value
fmt.Println(e.String)
// If TEST_UINT16 is set, e.Uint16 will contain its value.
// Otherwise, it will contain the default value, 100.
fmt.Println(e.Uint16)
// Output:
// an example!
// 100
}
//// Export tests
type testConfigExport struct {
String string `env:"TEST_STRING"`
Int64 int64 `env:"TEST_INT64"`
Uint16 uint16 `env:"TEST_UINT16"`
Float64 float64 `env:"TEST_FLOAT64"`
Bool bool `env:"TEST_BOOL"`
Duration time.Duration `env:"TEST_DURATION"`
URL *url.URL `env:"TEST_URL"`
StringSlice []string `env:"TEST_STRING_SLICE"`
UnsetString string `env:"TEST_UNSET_STRING"`
UnsetInt64 int64 `env:"TEST_UNSET_INT64"`
UnsetDuration time.Duration `env:"TEST_UNSET_DURATION"`
UnsetURL *url.URL `env:"TEST_UNSET_URL"`
UnusedField string
unexportedField string //nolint:structcheck,unused
IgnoredPtr *bool `env:"TEST_IGNORED_POINTER"`
Nested nestedConfigExport
NestedPtr *nestedConfigExportPointer
NestedPtrUnset *nestedConfigExportPointer
NestedTwice nestedTwiceConfig
NoConfig noConfig
NoConfigPtr *noConfig
NoConfigPtrSet *noConfig
RequiredInt int `env:"TEST_REQUIRED_INT,required"`
DefaultBool bool `env:"TEST_DEFAULT_BOOL,default=true"`
DefaultInt int `env:"TEST_DEFAULT_INT,default=1234"`
DefaultDuration time.Duration `env:"TEST_DEFAULT_DURATION,default=24h"`
DefaultURL *url.URL `env:"TEST_DEFAULT_URL,default=http://example.com"`
DefaultIntSet int `env:"TEST_DEFAULT_INT_SET,default=99"`
DefaultIntSlice []int `env:"TEST_DEFAULT_INT_SLICE,default=99"`
}
type nestedConfigExport struct {
String string `env:"TEST_NESTED_STRING"`
}
type nestedConfigExportPointer struct {
String string `env:"TEST_NESTED_STRING_POINTER"`
}
type noConfig struct {
Int int
}
type nestedTwiceConfig struct {
Nested nestedConfigInner
}
type nestedConfigInner struct {
String string `env:"TEST_NESTED_TWICE_STRING"`
}
type testConfigStrict struct {
InvalidInt64Strict int64 `env:"TEST_INVALID_INT64,strict,default=1"`
InvalidInt64Implicit int64 `env:"TEST_INVALID_INT64_IMPLICIT,default=1"`
Nested struct {
InvalidInt64Strict int64 `env:"TEST_INVALID_INT64_NESTED,strict,required"`
InvalidInt64Implicit int64 `env:"TEST_INVALID_INT64_NESTED_IMPLICIT,required"`
}
}
func TestInvalidStrict(t *testing.T) {
cases := []struct {
decoder func(interface{}) error
rootValue string
nestedValue string
rootValueImplicit string
nestedValueImplicit string
pass bool
}{
{Decode, "1", "1", "1", "1", true},
{Decode, "1", "1", "1", "asdf", true},
{Decode, "1", "1", "asdf", "1", true},
{Decode, "1", "1", "asdf", "asdf", true},
{Decode, "1", "asdf", "1", "1", false},
{Decode, "asdf", "1", "1", "1", false},
{Decode, "asdf", "asdf", "1", "1", false},
{StrictDecode, "1", "1", "1", "1", true},
{StrictDecode, "asdf", "1", "1", "1", false},
{StrictDecode, "1", "asdf", "1", "1", false},
{StrictDecode, "1", "1", "asdf", "1", false},
{StrictDecode, "1", "1", "1", "asdf", false},
{StrictDecode, "asdf", "asdf", "1", "1", false},
{StrictDecode, "1", "asdf", "asdf", "1", false},
{StrictDecode, "1", "1", "asdf", "asdf", false},
{StrictDecode, "1", "asdf", "asdf", "asdf", false},
{StrictDecode, "asdf", "asdf", "asdf", "asdf", false},
}
for _, test := range cases {
os.Setenv("TEST_INVALID_INT64", test.rootValue)
os.Setenv("TEST_INVALID_INT64_NESTED", test.nestedValue)
os.Setenv("TEST_INVALID_INT64_IMPLICIT", test.rootValueImplicit)
os.Setenv("TEST_INVALID_INT64_NESTED_IMPLICIT", test.nestedValueImplicit)
var tc testConfigStrict
if err := test.decoder(&tc); test.pass != (err == nil) {
t.Fatalf("Have err=%s wanted pass=%v", err, test.pass)
}
}
}
func TestExport(t *testing.T) {
testFloat64 := fmt.Sprintf("%.48f", math.Pi)
testFloat64Output := strconv.FormatFloat(math.Pi, 'f', -1, 64)
testInt64 := fmt.Sprintf("%d", -(1 << 50))
os.Setenv("TEST_STRING", "foo")
os.Setenv("TEST_INT64", testInt64)
os.Setenv("TEST_UINT16", "60000")
os.Setenv("TEST_FLOAT64", testFloat64)
os.Setenv("TEST_BOOL", "true")
os.Setenv("TEST_DURATION", "10m")
os.Setenv("TEST_URL", "https://example.com")
os.Setenv("TEST_STRING_SLICE", "foo,bar")
os.Setenv("TEST_NESTED_STRING", "nest_foo")
os.Setenv("TEST_NESTED_STRING_POINTER", "nest_foo_ptr")
os.Setenv("TEST_NESTED_TWICE_STRING", "nest_twice_foo")
os.Setenv("TEST_REQUIRED_INT", "101")
os.Setenv("TEST_DEFAULT_INT_SET", "102")
os.Setenv("TEST_DEFAULT_INT_SLICE", "1,2,3")
var tc testConfigExport
tc.NestedPtr = &nestedConfigExportPointer{}
tc.NoConfigPtrSet = &noConfig{}
if err := Decode(&tc); err != nil {
t.Fatal(err)
}
rc, err := Export(&tc)
if err != nil {
t.Fatal(err)
}
expected := []*ConfigInfo{
&ConfigInfo{
Field: "String",
EnvVar: "TEST_STRING",
Value: "foo",
UsesEnv: true,
},
&ConfigInfo{
Field: "Int64",
EnvVar: "TEST_INT64",
Value: testInt64,
UsesEnv: true,
},
&ConfigInfo{
Field: "Uint16",
EnvVar: "TEST_UINT16",
Value: "60000",
UsesEnv: true,
},
&ConfigInfo{
Field: "Float64",
EnvVar: "TEST_FLOAT64",
Value: testFloat64Output,
UsesEnv: true,
},
&ConfigInfo{
Field: "Bool",
EnvVar: "TEST_BOOL",
Value: "true",
UsesEnv: true,
},
&ConfigInfo{
Field: "Duration",
EnvVar: "TEST_DURATION",
Value: "10m0s",
UsesEnv: true,
},
&ConfigInfo{
Field: "URL",
EnvVar: "TEST_URL",
Value: "https://example.com",
UsesEnv: true,
},
&ConfigInfo{
Field: "StringSlice",
EnvVar: "TEST_STRING_SLICE",
Value: "[foo bar]",
UsesEnv: true,
},
&ConfigInfo{
Field: "UnsetString",
EnvVar: "TEST_UNSET_STRING",
Value: "",
},
&ConfigInfo{
Field: "UnsetInt64",
EnvVar: "TEST_UNSET_INT64",
Value: "0",
},
&ConfigInfo{
Field: "UnsetDuration",
EnvVar: "TEST_UNSET_DURATION",
Value: "0s",
},
&ConfigInfo{
Field: "UnsetURL",
EnvVar: "TEST_UNSET_URL",
Value: "",
},
&ConfigInfo{
Field: "IgnoredPtr",
EnvVar: "TEST_IGNORED_POINTER",
Value: "",
},
&ConfigInfo{
Field: "Nested.String",
EnvVar: "TEST_NESTED_STRING",
Value: "nest_foo",
UsesEnv: true,
},
&ConfigInfo{
Field: "NestedPtr.String",
EnvVar: "TEST_NESTED_STRING_POINTER",
Value: "nest_foo_ptr",
UsesEnv: true,
},
&ConfigInfo{
Field: "NestedTwice.Nested.String",
EnvVar: "TEST_NESTED_TWICE_STRING",
Value: "nest_twice_foo",
UsesEnv: true,
},
&ConfigInfo{
Field: "RequiredInt",
EnvVar: "TEST_REQUIRED_INT",
Value: "101",
UsesEnv: true,
Required: true,
},
&ConfigInfo{
Field: "DefaultBool",
EnvVar: "TEST_DEFAULT_BOOL",
Value: "true",
DefaultValue: "true",
HasDefault: true,
},
&ConfigInfo{
Field: "DefaultInt",
EnvVar: "TEST_DEFAULT_INT",
Value: "1234",
DefaultValue: "1234",
HasDefault: true,
},
&ConfigInfo{
Field: "DefaultDuration",
EnvVar: "TEST_DEFAULT_DURATION",
Value: "24h0m0s",
DefaultValue: "24h",
HasDefault: true,
},
&ConfigInfo{
Field: "DefaultURL",
EnvVar: "TEST_DEFAULT_URL",
Value: "http://example.com",
DefaultValue: "http://example.com",
HasDefault: true,
},
&ConfigInfo{
Field: "DefaultIntSet",
EnvVar: "TEST_DEFAULT_INT_SET",
Value: "102",
DefaultValue: "99",
HasDefault: true,
UsesEnv: true,
},
&ConfigInfo{
Field: "DefaultIntSlice",
EnvVar: "TEST_DEFAULT_INT_SLICE",
Value: "[1 2 3]",
DefaultValue: "99",
HasDefault: true,
UsesEnv: true,
},
}
sort.Sort(ConfigInfoSlice(expected))
if len(rc) != len(expected) {
t.Fatalf("Have %d results, expected %d", len(rc), len(expected))
}
for n, v := range rc {
ci := expected[n]
if *ci != *v {
t.Fatalf("have %+v, expected %+v", v, ci)
}
}
}