featurebase/idk/header_test.go
Jacob Brinlee df7e813f01
SUP-302 (#2243)
* add ability to use Θ in all field names & with PQL
2023-02-22 11:30:14 -06:00

436 lines
12 KiB
Go

package idk
import (
"fmt"
"reflect"
"strings"
"testing"
"time"
"github.com/featurebasedb/featurebase/v3/logger"
)
func TestHeaderToField(t *testing.T) {
seven := int64(7)
tests := []struct {
name string
input string
exp Field
expErr string
expLog string
}{
{
name: "empty",
input: "",
expErr: ErrNoFieldSpec.Error(),
},
{
name: "no name",
input: "__String",
expErr: "field '__String' has no sourceName",
},
{
name: "justname",
input: "blah",
expErr: ErrNoFieldSpec.Error(),
},
{
name: "justname__",
input: "blah__",
expErr: "unknown field",
},
{
name: "unknown field",
input: "blah__Ztring",
expErr: "unknown field",
},
{
name: "string",
input: "a__String",
exp: StringField{NameVal: "a", DestNameVal: "a"},
},
{
name: "string-mutex-f",
input: "a__String_F",
exp: StringField{NameVal: "a", DestNameVal: "a"},
},
{
name: "string-mutex",
input: "a__String_T",
exp: StringField{NameVal: "a", DestNameVal: "a", Mutex: true},
},
{
name: "string-quantum",
input: "a__String_F_YM",
exp: StringField{NameVal: "a", DestNameVal: "a", Mutex: false, Quantum: "YM"},
},
{
name: "string-ttl",
input: "a__String_F_YM_30s",
exp: StringField{NameVal: "a", DestNameVal: "a", Mutex: false, Quantum: "YM", TTL: "30s"},
},
{
name: "string-extra_arg",
input: "a__String_F_YM_30s_Z",
exp: StringField{NameVal: "a", DestNameVal: "a", Mutex: false, Quantum: "YM", TTL: "30s"},
expLog: "ignoring extra arguments to StringField a__String_F_YM_30s_Z",
},
{
name: "unknown arg",
input: "a__String_Z",
expErr: "can't interpret 'Z' for StringField.Mutex for field 'a'",
},
{
name: "bool",
input: "z__Bool",
exp: BoolField{NameVal: "z", DestNameVal: "z"},
},
{
name: "bool-extra",
input: "z__Bool_extra_2",
exp: BoolField{NameVal: "z", DestNameVal: "z"},
expLog: "ignoring extra arguments to BoolField",
},
{
name: "id",
input: "myname__ID",
exp: IDField{NameVal: "myname", DestNameVal: "myname"},
},
{
name: "id-mutex",
input: "z__ID_T",
exp: IDField{NameVal: "z", DestNameVal: "z", Mutex: true},
},
{
name: "id-quantum",
input: "z__ID_F_YMD",
exp: IDField{NameVal: "z", DestNameVal: "z", Mutex: false, Quantum: "YMD"},
},
{
name: "id-ttl",
input: "z__ID_F_YMD_30s",
exp: IDField{NameVal: "z", DestNameVal: "z", Mutex: false, Quantum: "YMD", TTL: "30s"},
},
{
name: "id-extra_arg",
input: "z__ID_F_YMD_30s_Z",
exp: IDField{NameVal: "z", DestNameVal: "z", Mutex: false, Quantum: "YMD", TTL: "30s"},
expLog: "ignoring extra arguments to IDField z__ID_F_YMD_30s_Z",
},
{
name: "int",
input: "myname__Int",
exp: IntField{NameVal: "myname", DestNameVal: "myname"},
},
{
name: "int-min",
input: "myname__Int_7",
exp: IntField{NameVal: "myname", DestNameVal: "myname", Min: &seven},
},
{
name: "int-min-max",
input: "myname__Int_7_7",
exp: IntField{NameVal: "myname", DestNameVal: "myname", Min: &seven, Max: &seven},
},
{
name: "int-min-max-foreign",
input: "myname__Int_7_7_findex",
exp: IntField{NameVal: "myname", DestNameVal: "myname", Min: &seven, Max: &seven, ForeignIndex: "findex"},
},
{
name: "int-min-max-extra",
input: "myname__Int_7_7_z_",
exp: IntField{NameVal: "myname", DestNameVal: "myname", Min: &seven, Max: &seven, ForeignIndex: "z"},
expLog: "ignoring extra arguments to IntField",
},
{
name: "int-min-parseerr",
input: "myname__Int_7_8.9",
expErr: "parsing max for",
},
{
name: "int-parserr-max",
input: "myname__Int_blah_7",
expErr: "parsing min for",
},
{
name: "decimal-scale",
input: "myname__Decimal_7",
exp: DecimalField{NameVal: "myname", DestNameVal: "myname", Scale: 7},
},
{
name: "decimal",
input: "myname__Decimal",
exp: DecimalField{NameVal: "myname", DestNameVal: "myname"},
},
{
name: "decimal-err",
input: "myname__Decimal_!",
expErr: "parsing scale for",
},
{
name: "stringarray-quantum",
input: "myname__StringArray_YMD",
exp: StringArrayField{NameVal: "myname", DestNameVal: "myname", Quantum: "YMD"},
},
{
name: "stringarray-ttl",
input: "myname__StringArray_YMD_30s",
exp: StringArrayField{NameVal: "myname", DestNameVal: "myname", Quantum: "YMD", TTL: "30s"},
},
{
name: "stringarray-extra_arg",
input: "myname__StringArray_YMD_30s_Z",
exp: StringArrayField{NameVal: "myname", DestNameVal: "myname", Quantum: "YMD", TTL: "30s"},
expLog: "ignoring extra arguments to StringArrayField",
},
{
name: "idarray-quantum",
input: "myname__IDArray_YMD",
exp: IDArrayField{NameVal: "myname", DestNameVal: "myname", Quantum: "YMD"},
},
{
name: "idarray-ttl",
input: "myname__IDArray_YMD_30s",
exp: IDArrayField{NameVal: "myname", DestNameVal: "myname", Quantum: "YMD", TTL: "30s"},
},
{
name: "idarray-extra_arg",
input: "myname__IDArray_YMD_30s_Z",
exp: IDArrayField{NameVal: "myname", DestNameVal: "myname", Quantum: "YMD", TTL: "30s"},
expLog: "ignoring extra arguments to IDArrayField",
},
{
name: "date-int",
input: "myname__DateInt",
exp: DateIntField{
NameVal: "myname",
DestNameVal: "myname",
Layout: time.RFC3339,
},
},
{
name: "date-int-layout-epoch",
input: "myname__DateInt_2006-01-02T15:04:05Z07:00_2018-03-04T15:04:05Z",
exp: DateIntField{
NameVal: "myname",
DestNameVal: "myname",
Layout: time.RFC3339,
Epoch: time.Date(2018, time.March, 4, 15, 4, 5, 0, time.UTC),
},
},
{
name: "date-int-layout-epoch-unit",
input: "myname__DateInt_2006-01-02T15:04:05Z07:00_2018-03-04T15:04:05Z_D",
exp: DateIntField{
NameVal: "myname", DestNameVal: "myname",
Layout: time.RFC3339,
Epoch: time.Date(2018, time.March, 4, 15, 4, 5, 0, time.UTC),
Unit: Day,
},
},
{
name: "date-int-layout-epoch-unit-custom",
input: "myname__DateInt_2006-01-02T15:04:05Z07:00_2018-03-04T15:04:05Z_C_10h",
exp: DateIntField{
NameVal: "myname", DestNameVal: "myname",
Layout: time.RFC3339,
Epoch: time.Date(2018, time.March, 4, 15, 4, 5, 0, time.UTC),
Unit: Custom,
CustomUnit: "10h",
},
},
{
name: "date-int-layout-epoch-unit-custom-2",
input: "myname__DateInt_2006-01-02_2018-03-04_D_0",
exp: DateIntField{
NameVal: "myname", DestNameVal: "myname",
Layout: "2006-01-02",
Epoch: time.Date(2018, time.March, 4, 0, 0, 0, 0, time.UTC),
Unit: Day,
},
},
{
name: "date-int-layout-epoch-unit-custom-error",
input: "myname__DateInt_2006-01-02_2018-03d-04_D_0",
expErr: `cannot parse "d-04"`,
},
{
name: "date-int-layout-epoch-unit-custom-layout-badunit",
input: "myname__DateInt_2006-01-02_2018-03-04_Ze_0",
expErr: fmt.Sprintf(ErrFmtUnknownUnit, "ze"),
},
{
name: "date-int-layout-epoch-unit-custom-layout-customuniterror",
input: "myname__DateInt_2006-01-02_2018-03-04_C_127z",
expErr: `parsing custom unit 127z: time: unknown unit "z" in duration "127z"`,
},
{
name: "recordtime",
input: "myname__RecordTime",
exp: RecordTimeField{NameVal: "myname", Layout: "2006-01-02T15:04:05Z07:00", DestNameVal: "myname"},
},
{
name: "recordtime",
input: "__RecordTime",
exp: RecordTimeField{Layout: "2006-01-02T15:04:05Z07:00"},
},
{
name: "recordtime",
input: "myname__RecordTime_2006-01-02",
exp: RecordTimeField{NameVal: "myname", DestNameVal: "myname", Layout: "2006-01-02"},
},
{
name: "recordtime-layout-epoch-unit-custom-error",
input: "myname__RecordTime_2006-01-02_2018-03d-04_D_0",
expErr: `cannot parse "d-04"`,
},
{
name: "recordtime-layout-epoch-unit-custom-layout-badunit",
input: "myname__RecordTime_2006-01-02_2018-03-04_Ze_0",
expErr: "unknown unit",
},
{
name: "multidunder",
input: "multi__dunder__String",
exp: StringField{NameVal: "multi__dunder", DestNameVal: "multi__dunder"},
},
{
name: "multi-dunder-no-fieldspec",
input: "multi__dunder__funder",
expErr: "unknown field 'funder' for",
},
{
name: "map-incompatible-name",
input: "@rbitrary.name*string!___pilosa-name-string__String",
exp: StringField{
NameVal: "@rbitrary.name*string!",
DestNameVal: "pilosa-name-string",
},
},
{
name: "map-compatible-name",
input: "dunderful__name__string___pilosa-name-string__String",
exp: StringField{
NameVal: "dunderful__name__string",
DestNameVal: "pilosa-name-string",
},
},
{
name: "map-dunderful-name",
input: "dunderful__name__string___pilosa__name__string__String",
exp: StringField{
NameVal: "dunderful__name__string",
DestNameVal: "pilosa__name__string",
},
},
{
name: "timestamp",
input: "purchasedate__Timestamp_ms",
exp: TimestampField{
NameVal: "purchasedate",
DestNameVal: "purchasedate",
Granularity: "ms",
Layout: time.RFC3339Nano,
},
},
{
name: "timestamp-epoch",
input: "purchasedate__Timestamp_s_2006-01-02T15:04:05Z07:00_2018-03-04T15:04:05Z_ms",
exp: TimestampField{
NameVal: "purchasedate",
DestNameVal: "purchasedate",
Granularity: "s",
Layout: time.RFC3339,
Epoch: time.Date(2018, time.March, 4, 15, 4, 5, 0, time.UTC),
Unit: Millisecond,
},
},
{
name: "lookup-text",
input: "a__LookupText",
exp: LookupTextField{NameVal: "a", DestNameVal: "a"},
},
{
name: "theta",
input: "fldΘnameΘ__String",
exp: StringField{NameVal: "fldΘnameΘ", DestNameVal: "fldΘnameΘ"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rl := &recordingLogger{}
field, err := HeaderToField(test.input, rl)
if test.expErr != "" {
if err == nil || !strings.Contains(err.Error(), test.expErr) {
t.Fatalf("unexpected error exp/got:\n%s\n%v", test.expErr, err)
}
} else if err != nil {
t.Fatalf("unexpected error: %v", err)
}
errorIfNotEqual(t, test.exp, field)
if test.expLog == "" && len(rl.logs) > 0 {
t.Errorf("unexpected logs: %+v", rl.logs)
} else if test.expLog != "" && len(rl.logs) == 0 {
t.Errorf("expected log: '%s'", test.expLog)
} else if test.expLog != "" {
if !strings.Contains(rl.logs[0], test.expLog) {
t.Errorf("exp/got:\n%s\n%s", test.expLog, rl.logs[0])
}
}
})
}
}
func errorIfNotEqual(t *testing.T, exp, got interface{}) {
t.Helper()
if !reflect.DeepEqual(exp, got) {
t.Errorf("unequal exp/got:\n%+v of %[1]T\n%+v of %[2]T", exp, got)
}
}
func errorIfNotErrMatch(t *testing.T, expErrSubstring string, got error) {
t.Helper()
if expErrSubstring == "" && got != nil {
t.Fatalf("unexpected error: %v", got)
} else if expErrSubstring == "" {
return
} else if expErrSubstring != "" && got == nil {
t.Fatalf("expected error like '%s'", expErrSubstring)
} else if !strings.Contains(got.Error(), expErrSubstring) {
t.Fatalf("unexpected error exp/got:\n%s\n%v", expErrSubstring, got)
}
}
type recordingLogger struct {
logs []string
debugs []string
}
func (r *recordingLogger) Panicf(format string, v ...interface{}) {
r.logs = append(r.logs, fmt.Sprintf("PANIC:"+format, v...))
panic(r.logs)
}
func (r *recordingLogger) Errorf(format string, v ...interface{}) {
r.Printf("ERROR:"+format, v)
}
func (r *recordingLogger) Infof(format string, v ...interface{}) {
r.Printf("INFO:"+format, v)
}
func (r *recordingLogger) Warnf(format string, v ...interface{}) {
r.Printf("WARN:"+format, v)
}
func (r *recordingLogger) Printf(format string, v ...interface{}) {
r.logs = append(r.logs, fmt.Sprintf(format, v...))
}
func (r *recordingLogger) Debugf(format string, v ...interface{}) {
r.debugs = append(r.debugs, fmt.Sprintf(format, v...))
}
// WithPrefix does nothing for the recording logger... just makes it
// implement the interface. Do not use.
func (r *recordingLogger) WithPrefix(prefix string) logger.Logger {
panic("unimplemented")
}