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") }