From 552a49a404d848c308a5c2909a03f24626a16d5c Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 13 Apr 2016 09:33:10 -0600 Subject: [PATCH] Alert data outputs to children nodes --- CHANGELOG.md | 22 ++ alert.go | 86 ++++- integrations/batcher_test.go | 117 +++++++ integrations/streamer_test.go | 24 +- node.go | 3 + pipeline/alert.go | 42 ++- pipeline/batch.go | 52 +-- pipeline/node.go | 2 + pipeline/stream.go | 63 +--- pipeline/udf.go | 2 +- tick/eval.go | 178 ++++++++-- tick/eval_test.go | 616 ++++++++++++++++++++++++++++++++++ 12 files changed, 1069 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2ff2752..48f7fe0d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,27 @@ Example UDF config for a socket based UDF. timeout = "10s" ``` +Alert data can now be consumed directly from within TICKscripts. +For example, let's say we want to store all data that triggered an alert in InfluxDB with a tag `level` containing the level string value (i.e CRITICAL). + +```javascript +... + |alert() + .warn(...) + .crit(...) + .levelTag('level') + // and/or use a field + //.levelField('level') + // Also tag the data with the alert ID + .idTag('id') + // and/or use a field + //.idField('id') + |influxDBOut() + .database('alerts') + ... +``` + + ### Features - [#360](https://github.com/influxdata/kapacitor/pull/360): Forking tasks by measurement in order to improve performance @@ -25,6 +46,7 @@ Example UDF config for a socket based UDF. - [#399](https://github.com/influxdata/kapacitor/issues/399): Allow disabling of subscriptions. - [#417](https://github.com/influxdata/kapacitor/issues/417): UDFs can be connected over a Unix socket. This enables UDFs from across Docker containers. - [#451](https://github.com/influxdata/kapacitor/issues/451): StreamNode supports `|groupBy` and `|where` methods. +- [#93](https://github.com/influxdata/kapacitor/issues/93): AlertNode now outputs data to child nodes. The output data can have either a tag or field indicating the alert level. ### Bugfixes diff --git a/alert.go b/alert.go index dab093b41..95d8812df 100644 --- a/alert.go +++ b/alert.go @@ -318,32 +318,66 @@ func (a *AlertNode) runAlert([]byte) error { return err } a.handleAlert(ad) + if a.a.LevelTag != "" || a.a.IdTag != "" { + p.Tags = p.Tags.Copy() + if a.a.LevelTag != "" { + p.Tags[a.a.LevelTag] = l.String() + } + if a.a.IdTag != "" { + p.Tags[a.a.IdTag] = ad.ID + } + } + if a.a.LevelField != "" || a.a.IdField != "" { + p.Fields = p.Fields.Copy() + if a.a.LevelField != "" { + p.Fields[a.a.LevelField] = l.String() + } + if a.a.IdField != "" { + p.Fields[a.a.IdField] = ad.ID + } + } + a.timer.Pause() + for _, child := range a.outs { + err := child.CollectPoint(p) + if err != nil { + return err + } + } + a.timer.Resume() } a.timer.Stop() } case pipeline.BatchEdge: for b, ok := a.ins[0].NextBatch(); ok; b, ok = a.ins[0].NextBatch() { a.timer.Start() + allOK := true triggered := false + var l AlertLevel + var id string for _, p := range b.Points { - l := a.determineLevel(p.Time, p.Fields, p.Tags) + l = a.determineLevel(p.Time, p.Fields, p.Tags) if l > OKAlert { - triggered = true + allOK = false state := a.updateState(l, b.Group) if (a.a.UseFlapping && state.flapping) || (a.a.IsStateChangesOnly && !state.changed) { break } + triggered = true ad, err := a.alertData(b.Name, b.Group, b.Tags, p.Fields, l, p.Time, b) if err != nil { return err } + id = ad.ID a.handleAlert(ad) break } } - if !triggered { + // Check for recovery + if allOK { + l = OKAlert state := a.updateState(OKAlert, b.Group) if state.changed { + triggered = true var fields models.Fields if l := len(b.Points); l > 0 { fields = b.Points[l-1].Fields @@ -352,9 +386,55 @@ func (a *AlertNode) runAlert([]byte) error { if err != nil { return err } + id = ad.ID a.handleAlert(ad) } } + if triggered { + // Update tags or fields for Level property + if a.a.LevelTag != "" || + a.a.LevelField != "" || + a.a.IdTag != "" || + a.a.IdField != "" { + for i := range b.Points { + if a.a.LevelTag != "" || a.a.IdTag != "" { + b.Points[i].Tags = b.Points[i].Tags.Copy() + if a.a.LevelTag != "" { + b.Points[i].Tags[a.a.LevelTag] = l.String() + } + if a.a.IdTag != "" { + b.Points[i].Tags[a.a.IdTag] = id + } + } + if a.a.LevelField != "" || a.a.IdField != "" { + b.Points[i].Fields = b.Points[i].Fields.Copy() + if a.a.LevelField != "" { + b.Points[i].Fields[a.a.LevelField] = l.String() + } + if a.a.IdField != "" { + b.Points[i].Fields[a.a.IdField] = id + } + } + } + if a.a.LevelTag != "" || a.a.IdTag != "" { + b.Tags = b.Tags.Copy() + if a.a.LevelTag != "" { + b.Tags[a.a.LevelTag] = l.String() + } + if a.a.IdTag != "" { + b.Tags[a.a.IdTag] = id + } + } + } + a.timer.Pause() + for _, child := range a.outs { + err := child.CollectBatch(b) + if err != nil { + return err + } + } + a.timer.Resume() + } a.timer.Stop() } } diff --git a/integrations/batcher_test.go b/integrations/batcher_test.go index a10c35ed1..ab29cf945 100644 --- a/integrations/batcher_test.go +++ b/integrations/batcher_test.go @@ -284,6 +284,123 @@ batch testBatcherWithOutput(t, "TestBatch_SimpleMR", script, 30*time.Second, er) } +func TestBatch_AlertLevelField(t *testing.T) { + + var script = ` +batch + |query(''' + SELECT mean("value") + FROM "telegraf"."default".cpu_usage_idle + WHERE "host" = 'serverA' AND "cpu" != 'cpu-total' +''') + .period(10s) + .every(10s) + .groupBy(time(2s), 'cpu') + |alert() + .crit(lambda:"mean" > 95) + .levelField('level') + .idField('id') + |httpOut('TestBatch_SimpleMR') +` + + er := kapacitor.Result{ + Series: imodels.Rows{ + { + Name: "cpu_usage_idle", + Tags: map[string]string{"cpu": "cpu1"}, + Columns: []string{"time", "id", "level", "mean"}, + Values: [][]interface{}{ + { + time.Date(1971, 1, 1, 0, 0, 20, 0, time.UTC), + "cpu_usage_idle:cpu=cpu1,", + "CRITICAL", + 96.49999999996908, + }, + { + time.Date(1971, 1, 1, 0, 0, 22, 0, time.UTC), + "cpu_usage_idle:cpu=cpu1,", + "CRITICAL", + 93.46464646468584, + }, + { + time.Date(1971, 1, 1, 0, 0, 24, 0, time.UTC), + "cpu_usage_idle:cpu=cpu1,", + "CRITICAL", + 95.00950095007724, + }, + { + time.Date(1971, 1, 1, 0, 0, 26, 0, time.UTC), + "cpu_usage_idle:cpu=cpu1,", + "CRITICAL", + 92.99999999998636, + }, + { + time.Date(1971, 1, 1, 0, 0, 28, 0, time.UTC), + "cpu_usage_idle:cpu=cpu1,", + "CRITICAL", + 90.99999999998545, + }, + }, + }, + }, + } + + testBatcherWithOutput(t, "TestBatch_SimpleMR", script, 30*time.Second, er) +} + +func TestBatch_AlertLevelTag(t *testing.T) { + + var script = ` +batch + |query(''' + SELECT mean("value") + FROM "telegraf"."default".cpu_usage_idle + WHERE "host" = 'serverA' AND "cpu" != 'cpu-total' +''') + .period(10s) + .every(10s) + .groupBy(time(2s), 'cpu') + |alert() + .crit(lambda:"mean" > 95) + .levelTag('level') + .idTag('id') + |httpOut('TestBatch_SimpleMR') +` + + er := kapacitor.Result{ + Series: imodels.Rows{ + { + Name: "cpu_usage_idle", + Tags: map[string]string{"cpu": "cpu1", "level": "CRITICAL", "id": "cpu_usage_idle:cpu=cpu1,"}, + Columns: []string{"time", "mean"}, + Values: [][]interface{}{ + { + time.Date(1971, 1, 1, 0, 0, 20, 0, time.UTC), + 96.49999999996908, + }, + { + time.Date(1971, 1, 1, 0, 0, 22, 0, time.UTC), + 93.46464646468584, + }, + { + time.Date(1971, 1, 1, 0, 0, 24, 0, time.UTC), + 95.00950095007724, + }, + { + time.Date(1971, 1, 1, 0, 0, 26, 0, time.UTC), + 92.99999999998636, + }, + { + time.Date(1971, 1, 1, 0, 0, 28, 0, time.UTC), + 90.99999999998545, + }, + }, + }, + }, + } + + testBatcherWithOutput(t, "TestBatch_SimpleMR", script, 30*time.Second, er) +} func TestBatch_Join(t *testing.T) { diff --git a/integrations/streamer_test.go b/integrations/streamer_test.go index 78a0cdcc5..cf190adbd 100644 --- a/integrations/streamer_test.go +++ b/integrations/streamer_test.go @@ -2184,13 +2184,35 @@ stream |alert() .id('kapacitor/{{ .Name }}/{{ index .Tags "host" }}') .details('details') + .idField('id') + .idTag('id') + .levelField('level') + .levelTag('level') .info(lambda: "count" > infoThreshold) .warn(lambda: "count" > warnThreshold) .crit(lambda: "count" > critThreshold) .post('` + ts.URL + `') + |log() + |httpOut('TestStream_Alert') ` - testStreamerNoOutput(t, "TestStream_Alert", script, 13*time.Second) + er := kapacitor.Result{ + Series: imodels.Rows{ + { + Name: "cpu", + Tags: map[string]string{"host": "serverA", "level": "CRITICAL", "id": "kapacitor/cpu/serverA"}, + Columns: []string{"time", "count", "id", "level"}, + Values: [][]interface{}{[]interface{}{ + time.Date(1971, 1, 1, 0, 0, 10, 0, time.UTC), + 10.0, + "kapacitor/cpu/serverA", + "CRITICAL", + }}, + }, + }, + } + + testStreamerWithOutput(t, "TestStream_Alert", script, 13*time.Second, er, nil, false) if rc := atomic.LoadInt32(&requestCount); rc != 1 { t.Errorf("got %v exp %v", rc, 1) diff --git a/node.go b/node.go index 9cd106513..2db2cdf55 100644 --- a/node.go +++ b/node.go @@ -156,6 +156,9 @@ func (n *node) addChild(c Node) (*Edge, error) { if n.Provides() != c.Wants() { return nil, fmt.Errorf("cannot add child mismatched edges: %s:%s -> %s:%s", n.Name(), n.Provides(), c.Name(), c.Wants()) } + if n.Provides() == pipeline.NoEdge { + return nil, fmt.Errorf("cannot add child no edge expected: %s:%s -> %s:%s", n.Name(), n.Provides(), c.Name(), c.Wants()) + } n.children = append(n.children, c) edge := newEdge(n.et.Task.Name, n.Name(), c.Name(), n.Provides(), defaultEdgeBufferSize, n.et.tm.LogService) diff --git a/pipeline/alert.go b/pipeline/alert.go index e70a3929d..f2f8f716e 100644 --- a/pipeline/alert.go +++ b/pipeline/alert.go @@ -1,6 +1,10 @@ package pipeline -import "github.com/influxdata/kapacitor/tick" +import ( + "reflect" + + "github.com/influxdata/kapacitor/tick" +) // Number of previous states to remember when computing flapping percentage. const defaultFlapHistory = 21 @@ -81,7 +85,7 @@ const defaultLogFileMode = 0600 // CRITICAL expression. // Each expression maintains its own state. type AlertNode struct { - node + chainnode // Template for constructing a unique ID for a given alert. // @@ -205,6 +209,16 @@ type AlertNode struct { // Default: 21 History int64 + // Optional tag key to use when tagging the data with the alert level. + LevelTag string + // Optional field key to add to the data, containing the alert level as a string. + LevelField string + + // Optional tag key to use when tagging the data with the alert ID. + IdTag string + // Optional field key to add to the data, containing the alert ID as a string. + IdField string + // Send alerts only on state changes. // tick:ignore IsStateChangesOnly bool `tick:"StateChangesOnly"` @@ -259,16 +273,20 @@ type AlertNode struct { } func newAlertNode(wants EdgeType) *AlertNode { - return &AlertNode{ - node: node{ - desc: "alert", - wants: wants, - provides: NoEdge, - }, - History: defaultFlapHistory, - Id: defaultIDTmpl, - Message: defaultMessageTmpl, - Details: defaultDetailsTmpl, + a := &AlertNode{ + chainnode: newBasicChainNode("alert", wants, wants), + History: defaultFlapHistory, + Id: defaultIDTmpl, + Message: defaultMessageTmpl, + Details: defaultDetailsTmpl, + } + return a +} + +//tick:ignore +func (n *AlertNode) ChainMethods() map[string]reflect.Value { + return map[string]reflect.Value{ + "Log": reflect.ValueOf(n.chainnode.Log), } } diff --git a/pipeline/batch.go b/pipeline/batch.go index 00ae96410..d754bd1bc 100644 --- a/pipeline/batch.go +++ b/pipeline/batch.go @@ -2,9 +2,8 @@ package pipeline import ( "bytes" + "reflect" "time" - - "github.com/influxdata/kapacitor/tick" ) // A node that handles creating several child BatchNodes. @@ -74,9 +73,6 @@ func (b *SourceBatchNode) dot(buf *bytes.Buffer) { type BatchNode struct { chainnode - // self describer - describer *tick.ReflectionDescriber - // The query text //tick:ignore QueryStr string @@ -133,10 +129,16 @@ func newBatchNode() *BatchNode { b := &BatchNode{ chainnode: newBasicChainNode("batch", BatchEdge, BatchEdge), } - b.describer, _ = tick.NewReflectionDescriber(b) return b } +//tick:ignore +func (n *BatchNode) ChainMethods() map[string]reflect.Value { + return map[string]reflect.Value{ + "GroupBy": reflect.ValueOf(n.chainnode.GroupBy), + } +} + // Group the data by a set of dimensions. // Can specify one time dimension. // @@ -162,41 +164,3 @@ func (b *BatchNode) Align() *BatchNode { b.AlignFlag = true return b } - -// Tick Describer methods - -//tick:ignore -func (b *BatchNode) Desc() string { - return b.describer.Desc() -} - -//tick:ignore -func (b *BatchNode) HasChainMethod(name string) bool { - if name == "groupBy" { - return true - } - return b.describer.HasChainMethod(name) -} - -//tick:ignore -func (b *BatchNode) CallChainMethod(name string, args ...interface{}) (interface{}, error) { - if name == "groupBy" { - return b.chainnode.GroupBy(args...), nil - } - return b.describer.CallChainMethod(name, args...) -} - -//tick:ignore -func (b *BatchNode) HasProperty(name string) bool { - return b.describer.HasProperty(name) -} - -//tick:ignore -func (b *BatchNode) Property(name string) interface{} { - return b.describer.Property(name) -} - -//tick:ignore -func (b *BatchNode) SetProperty(name string, args ...interface{}) (interface{}, error) { - return b.describer.SetProperty(name, args...) -} diff --git a/pipeline/node.go b/pipeline/node.go index 970291a95..f4a72fd27 100644 --- a/pipeline/node.go +++ b/pipeline/node.go @@ -25,6 +25,8 @@ type ID int func (e EdgeType) String() string { switch e { + case NoEdge: + return "noedge" case StreamEdge: return "stream" case BatchEdge: diff --git a/pipeline/stream.go b/pipeline/stream.go index a2405b302..9a5b8ca8b 100644 --- a/pipeline/stream.go +++ b/pipeline/stream.go @@ -1,7 +1,7 @@ package pipeline import ( - "fmt" + "reflect" "time" "github.com/influxdata/kapacitor/tick" @@ -74,9 +74,6 @@ func (s *SourceStreamNode) From() *StreamNode { type StreamNode struct { chainnode - // self describer - describer *tick.ReflectionDescriber - // An expression to filter the data stream. // tick:ignore Expression tick.Node `tick:"Where"` @@ -113,11 +110,18 @@ func newStreamNode() *StreamNode { s := &StreamNode{ chainnode: newBasicChainNode("stream", StreamEdge, StreamEdge), } - s.describer, _ = tick.NewReflectionDescriber(s) return s } +//tick:ignore +func (n *StreamNode) ChainMethods() map[string]reflect.Value { + return map[string]reflect.Value{ + "GroupBy": reflect.ValueOf(n.chainnode.GroupBy), + "Where": reflect.ValueOf(n.chainnode.Where), + } +} + // Creates a new stream node that can be further // filtered using the Database, RetentionPolicy, Measurement and Where properties. // From can be called multiple times to create multiple @@ -233,52 +237,3 @@ func (s *StreamNode) GroupBy(tag ...interface{}) *StreamNode { s.Dimensions = tag return s } - -// Tick Describer methods - -//tick:ignore -func (s *StreamNode) Desc() string { - return s.describer.Desc() -} - -//tick:ignore -func (s *StreamNode) HasChainMethod(name string) bool { - if name == "groupBy" || name == "where" { - return true - } - return s.describer.HasChainMethod(name) -} - -//tick:ignore -func (s *StreamNode) CallChainMethod(name string, args ...interface{}) (interface{}, error) { - switch name { - case "groupBy": - return s.chainnode.GroupBy(args...), nil - case "where": - if len(args) != 1 { - return nil, fmt.Errorf("invalid number of args to |where() got %d exp 1", len(args)) - } - expr, ok := args[0].(tick.Node) - if !ok { - return nil, fmt.Errorf("invalid arg to |where() got %T exp tick.Node", args[0]) - } - return s.chainnode.Where(expr), nil - default: - return s.describer.CallChainMethod(name, args...) - } -} - -//tick:ignore -func (s *StreamNode) HasProperty(name string) bool { - return s.describer.HasProperty(name) -} - -//tick:ignore -func (s *StreamNode) Property(name string) interface{} { - return s.describer.Property(name) -} - -//tick:ignore -func (s *StreamNode) SetProperty(name string, args ...interface{}) (interface{}, error) { - return s.describer.SetProperty(name, args...) -} diff --git a/pipeline/udf.go b/pipeline/udf.go index d9fc2b550..75a4d54d2 100644 --- a/pipeline/udf.go +++ b/pipeline/udf.go @@ -82,7 +82,7 @@ func NewUDF( UDFName: name, options: options, } - udf.describer, _ = tick.NewReflectionDescriber(udf) + udf.describer, _ = tick.NewReflectionDescriber(udf, nil) parent.linkChild(udf) return udf } diff --git a/tick/eval.go b/tick/eval.go index 3ac1f7ead..b77b0f368 100644 --- a/tick/eval.go +++ b/tick/eval.go @@ -1,7 +1,9 @@ package tick import ( + "errors" "fmt" + "go/ast" "log" "os" "reflect" @@ -43,6 +45,12 @@ type SelfDescriber interface { SetProperty(name string, args ...interface{}) (interface{}, error) } +// PartialDescriber can provide a description +// of its chain methods that hide embedded property methods. +type PartialDescriber interface { + ChainMethods() map[string]reflect.Value +} + // Parse and evaluate a given script for the scope. // This evaluation method uses reflection to call // methods on objects within the scope. @@ -268,7 +276,11 @@ func evalChain(p Position, scope *Scope, stck *stack) error { describer = d } else { var err error - describer, err = NewReflectionDescriber(l) + var extraChainMethods map[string]reflect.Value + if pd, ok := l.(PartialDescriber); ok { + extraChainMethods = pd.ChainMethods() + } + describer, err = NewReflectionDescriber(l, extraChainMethods) if err != nil { return wrapError(p, err) } @@ -320,7 +332,11 @@ func evalFunc(f *FunctionNode, scope *Scope, stck *stack, args []interface{}) er describer = d } else { var err error - describer, err = NewReflectionDescriber(obj) + var extraChainMethods map[string]reflect.Value + if pd, ok := obj.(PartialDescriber); ok { + extraChainMethods = pd.ChainMethods() + } + describer, err = NewReflectionDescriber(obj, extraChainMethods) if err != nil { return nil, wrapError(f, err) } @@ -390,6 +406,26 @@ func evalFunc(f *FunctionNode, scope *Scope, stck *stack, args []interface{}) er } // Wraps any object as a SelfDescriber using reflection. +// +// Uses tags on fields to determine if a method is really a PropertyMethod +// Can disambiguate property fields and chain methods of the same name but +// from different composed anonymous fields. +// Cannot disambiguate property methods and chain methods of the same name. +// See NewReflectionDescriber for providing explicit chain methods in this case. +// +// Example: +// type MyType struct { +// UseX `tick:"X"` +// } +// func (m *MyType) X() *MyType{ +// m.UseX = true +// return m +// } +// +// UseX will be ignored as a property and the method X will become a property method. +// +// +// Expects that all callable methods are pointer receiver methods. type ReflectionDescriber struct { obj interface{} // Set of chain methods @@ -400,7 +436,36 @@ type ReflectionDescriber struct { properties map[string]reflect.Value } -func NewReflectionDescriber(obj interface{}) (*ReflectionDescriber, error) { +// Create a NewReflectionDescriber from an object. +// The object must be a pointer type. +// Use the chainMethods parameter to provide a set of explicit methods +// that should be considered chain methods even if an embedded type declares them as property methods +// +// Example: +// type MyType struct { +// UseX `tick:"X"` +// } +// func (m *MyType) X() *MyType{ +// m.UseX = true +// return m +// } +// +// type AnotherType struct { +// MyType +// } +// func (a *AnotherType) X() *YetAnotherType { +// // do chain method work here... +// } +// +// // Now create NewReflectionDescriber with X as a chain method and property method +// at := new(AnotherType) +// rd := NewReflectionDescriber(at, map[string]reflect.Value{ +// "X": reflect.ValueOf(at.X), +// }) +// rd.HasProperty("x") // true +// rd.HasChainMethod("x") // true +// +func NewReflectionDescriber(obj interface{}, chainMethods map[string]reflect.Value) (*ReflectionDescriber, error) { r := &ReflectionDescriber{ obj: obj, } @@ -408,34 +473,47 @@ func NewReflectionDescriber(obj interface{}) (*ReflectionDescriber, error) { if !rv.IsValid() && rv.Kind() != reflect.Struct { return nil, fmt.Errorf("object is invalid %v of type %T", obj, obj) } - rStructType := reflect.Indirect(rv).Type() - rRecvType := reflect.TypeOf(r.obj) - // Get all methods - r.chainMethods = make(map[string]reflect.Value, rRecvType.NumMethod()) - for i := 0; i < rRecvType.NumMethod(); i++ { - method := rRecvType.Method(i) - if !rv.MethodByName(method.Name).IsValid() { - return nil, fmt.Errorf("invalid method %s on type %T", method.Name, r.obj) - } - r.chainMethods[method.Name] = rv.MethodByName(method.Name) - } // Get all properties var err error - r.properties, r.propertyMethods, err = getProperties(r.Desc(), rv, rStructType, r.chainMethods) + r.properties, r.propertyMethods, err = getProperties(r.Desc(), rv) if err != nil { return nil, err } + + // Get all methods + r.chainMethods, err = getChainMethods(r.Desc(), rv, r.propertyMethods) + if err != nil { + return nil, err + } + for k, v := range chainMethods { + r.chainMethods[k] = v + } + return r, nil } -// Get properties from a struct and populate properties and propertyMethods maps, while removing -// and property methods from chainMethods. +// Get properties from a struct and populate properties and propertyMethods maps // Recurses up anonymous fields. -func getProperties(desc string, rv reflect.Value, rStructType reflect.Type, chainMethods map[string]reflect.Value) ( +func getProperties( + desc string, + rv reflect.Value, +) ( map[string]reflect.Value, map[string]reflect.Value, - error) { + error, +) { + if rv.Kind() != reflect.Ptr { + return nil, nil, errors.New("cannot get properties of non pointer value") + } + element := rv.Elem() + if !element.IsValid() { + return nil, nil, errors.New("cannot get properties of nil pointer") + } + rStructType := element.Type() + if rStructType.Kind() != reflect.Struct { + return nil, nil, errors.New("cannot get properties of non struct") + } properties := make(map[string]reflect.Value, rStructType.NumField()) propertyMethods := make(map[string]reflect.Value) for i := 0; i < rStructType.NumField(); i++ { @@ -443,8 +521,14 @@ func getProperties(desc string, rv reflect.Value, rStructType reflect.Type, chai if property.Anonymous { // Recursively get properties from anon fields anonValue := reflect.Indirect(rv).Field(i) - anonType := reflect.Indirect(anonValue).Type() - props, propMethods, err := getProperties(desc, anonValue, anonType, chainMethods) + if anonValue.Kind() != reflect.Ptr && anonValue.CanAddr() { + anonValue = anonValue.Addr() + } + if anonValue.Kind() == reflect.Ptr && anonValue.IsNil() { + // Skip nil fields + continue + } + props, propMethods, err := getProperties(fmt.Sprintf("%s.%s", desc, property.Name), anonValue) if err != nil { return nil, nil, err } @@ -465,10 +549,11 @@ func getProperties(desc string, rv reflect.Value, rStructType reflect.Type, chai if methodName != "" { // Property is set via a property method. method := rv.MethodByName(methodName) + if !method.IsValid() && rv.CanAddr() { + method = rv.Addr().MethodByName(methodName) + } if method.IsValid() { propertyMethods[methodName] = method - // Remove property method from chainMethods. - delete(chainMethods, methodName) } else { return nil, nil, fmt.Errorf("referenced method %s for type %s is invalid", methodName, desc) } @@ -483,6 +568,53 @@ func getProperties(desc string, rv reflect.Value, rStructType reflect.Type, chai return properties, propertyMethods, nil } +func getChainMethods(desc string, rv reflect.Value, propertyMethods map[string]reflect.Value) (map[string]reflect.Value, error) { + if rv.Kind() != reflect.Ptr { + return nil, errors.New("cannot get chain methods of non pointer") + } + element := rv.Elem() + if !element.IsValid() { + return nil, errors.New("cannot get chain methods of nil pointer") + } + // Find all methods on value + rRecvType := rv.Type() + chainMethods := make(map[string]reflect.Value, rRecvType.NumMethod()) + for i := 0; i < rRecvType.NumMethod(); i++ { + method := rRecvType.Method(i) + if !ast.IsExported(method.Name) { + continue + } + if !rv.MethodByName(method.Name).IsValid() { + return nil, fmt.Errorf("invalid method %s on type %s", method.Name, desc) + } + if _, exists := propertyMethods[method.Name]; !exists { + chainMethods[method.Name] = rv.MethodByName(method.Name) + } + } + + // Find all methods from anonymous fields. + rStructType := element.Type() + for i := 0; i < rStructType.NumField(); i++ { + field := rStructType.Field(i) + if field.Anonymous { + anonValue := element.Field(i) + if anonValue.Kind() != reflect.Ptr && anonValue.CanAddr() { + anonValue = anonValue.Addr() + } + anonChainMethods, err := getChainMethods(fmt.Sprintf("%s.%s", desc, field.Name), anonValue, propertyMethods) + if err != nil { + return nil, err + } + for k, v := range anonChainMethods { + if _, exists := chainMethods[k]; !exists { + chainMethods[k] = v + } + } + } + } + return chainMethods, nil +} + func (r *ReflectionDescriber) Desc() string { return fmt.Sprintf("%T", r.obj) } diff --git a/tick/eval_test.go b/tick/eval_test.go index 642430f43..c27b2f993 100644 --- a/tick/eval_test.go +++ b/tick/eval_test.go @@ -266,3 +266,619 @@ var s2 = a.structB() t.Fatal("expected error from Evaluate") } } + +//------------------------------------ +// Types for TestReflectionDescriber +// + +// Basic type +type A struct { + AProperty string + AFlag bool `tick:"PropertyMethodA"` + AHiddenPMFlag bool `tick:"HiddenPropertyMethod"` + propertyMethodCallCount int + chainMethodCallCount int + hcmCallCount int +} + +func (a *A) PropertyMethodA() *A { + a.AFlag = true + a.propertyMethodCallCount++ + return a +} + +func (a *A) ChainMethodA() *A { + a.chainMethodCallCount++ + return new(A) +} + +func (a *A) HiddenPropertyMethod() *A { + a.AHiddenPMFlag = true + return a +} + +func (a *A) HiddenChainMethod() *A { + a.hcmCallCount++ + return new(A) +} + +func (a *A) privateMethod() {} + +// Type that embeds A +type B struct { + A + BProperty string + BFlag bool `tick:"PropertyMethodB"` + BOverriddingProp string `tick:"HiddenChainMethod"` + + // Property hiding ChainMethodA + ChainMethodA string + + propertyMethodCallCount int + chainMethodCallCount int + apCallCount int + hpmCallCount int +} + +func (b *B) PropertyMethodB() *B { + b.BFlag = true + b.propertyMethodCallCount++ + return b +} + +func (b *B) ChainMethodB() *B { + b.chainMethodCallCount++ + return new(B) +} + +// Chain method hiding AProperty +func (b *B) AProperty() *B { + b.apCallCount++ + return new(B) +} + +// Chain method hiding A.HiddenPropertyMethod property method +func (b *B) HiddenPropertyMethod() *B { + b.hpmCallCount++ + return new(B) +} + +// Property method hidding chain method +func (b *B) HiddenChainMethod(value string) *B { + b.BOverriddingProp = value + return b +} + +// Type that embeds *A +type C struct { + *A + CProperty string + CFlag bool `tick:"PropertyMethodC"` + COverriddingProp string `tick:"HiddenChainMethod"` + + // Property hiding ChainMethodA + ChainMethodA string + + propertyMethodCallCount int + chainMethodCallCount int + apCallCount int + hpmCallCount int +} + +func (c *C) PropertyMethodC() *C { + c.CFlag = true + c.propertyMethodCallCount++ + return c +} + +func (c *C) ChainMethodC() (*C, error) { + c.chainMethodCallCount++ + return new(C), nil +} + +// Chain method hiding AProperty +func (c *C) AProperty() *C { + c.apCallCount++ + return new(C) +} + +// Chain method hiding A.HiddenPropertyMethod property method +func (c *C) HiddenPropertyMethod() *C { + c.hpmCallCount++ + return new(C) +} + +// Property method hidding chain method +func (c *C) HiddenChainMethod(value string) *C { + c.COverriddingProp = value + return c +} + +func TestReflectionDescriber(t *testing.T) { + //---------------- + // Test A type + // + a := new(A) + rdA, err := tick.NewReflectionDescriber(a, nil) + if err != nil { + t.Fatal(err) + } + + // Test A.privateMethod + if exp, got := false, rdA.HasProperty("privateMethod"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + + // Test A.AProperty + if exp, got := true, rdA.HasProperty("aProperty"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + if exp, got := false, rdA.HasChainMethod("aProperty"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdA.SetProperty("aProperty", "test") + if err != nil { + t.Fatal(err) + } + if exp, got := "test", a.AProperty; exp != got { + t.Fatalf("unexpected a.AProperty got: %v exp: %v", got, exp) + } + + // Test A.PropertyMethodA + if exp, got := true, rdA.HasProperty("propertyMethodA"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + if exp, got := false, rdA.HasChainMethod("propertyMethodA"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + if exp, got := false, rdA.HasProperty("aFlag"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdA.SetProperty("propertyMethodA") + if err != nil { + t.Fatal(err) + } + if exp, got := true, a.AFlag; exp != got { + t.Fatalf("unexpected a.AFlag got: %v exp: %v", got, exp) + } + if exp, got := 1, a.propertyMethodCallCount; exp != got { + t.Fatalf("unexpected a.propertyMethodCallCount got: %v exp: %v", got, exp) + } + + // Test A.HiddenPropertyMethod + if exp, got := true, rdA.HasProperty("hiddenPropertyMethod"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + if exp, got := false, rdA.HasChainMethod("hiddenPropertyMethod"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + if exp, got := false, rdA.HasProperty("aHiddenPMFlag"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdA.SetProperty("hiddenPropertyMethod") + if err != nil { + t.Fatal(err) + } + if exp, got := true, a.AHiddenPMFlag; exp != got { + t.Fatalf("unexpected a.AHiddenPMFlag got: %v exp: %v", got, exp) + } + + // Test A.ChainMethodA + if exp, got := true, rdA.HasChainMethod("chainMethodA"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + if exp, got := false, rdA.HasProperty("chainMethodA"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdA.CallChainMethod("chainMethodA") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, a.chainMethodCallCount; exp != got { + t.Fatalf("unexpected a.chainMethodCallCount got: %v exp: %v", got, exp) + } + + // Test A.HiddenChainMethod + if exp, got := true, rdA.HasChainMethod("hiddenChainMethod"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + if exp, got := false, rdA.HasProperty("hiddenChainMethod"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdA.CallChainMethod("hiddenChainMethod") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, a.hcmCallCount; exp != got { + t.Fatalf("unexpected a.hcmCallCount got: %v exp: %v", got, exp) + } + + //---------------- + // Test B type + // + b := new(B) + rdB, err := tick.NewReflectionDescriber(b, map[string]reflect.Value{ + "HiddenPropertyMethod": reflect.ValueOf(b.HiddenPropertyMethod), + "HiddenChainMethod": reflect.ValueOf(b.A.HiddenChainMethod), + }) + if err != nil { + t.Fatal(err) + } + + // Test B.AProperty as property + if exp, got := true, rdB.HasProperty("aProperty"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdB.SetProperty("aProperty", "test") + if err != nil { + t.Fatal(err) + } + if exp, got := "test", b.A.AProperty; exp != got { + t.Fatalf("unexpected b.A.AProperty got: %v exp: %v", got, exp) + } + + // Test B.AProperty as chain + if exp, got := true, rdB.HasChainMethod("aProperty"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdB.CallChainMethod("aProperty") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, b.apCallCount; exp != got { + t.Fatalf("unexpected b.apCallCount got: %v exp: %v", got, exp) + } + + // Test B.PropertyMethodA + if exp, got := true, rdB.HasProperty("propertyMethodA"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + if exp, got := false, rdB.HasChainMethod("propertyMethodA"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + if exp, got := false, rdB.HasProperty("aFlag"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdB.SetProperty("propertyMethodA") + if err != nil { + t.Fatal(err) + } + if exp, got := true, b.AFlag; exp != got { + t.Fatalf("unexpected b.AFlag got: %v exp: %v", got, exp) + } + if exp, got := 1, b.A.propertyMethodCallCount; exp != got { + t.Fatalf("unexpected b.A.propertyMethodCallCount got: %v exp: %v", got, exp) + } + + // Test B.HiddenPropertyMethod as chain + if exp, got := true, rdB.HasChainMethod("hiddenPropertyMethod"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdB.CallChainMethod("hiddenPropertyMethod") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, b.hpmCallCount; exp != got { + t.Fatalf("unexpected b.hpmCallCount got: %v exp: %v", got, exp) + } + + // Test B.HiddenPropertyMethod as property + if exp, got := true, rdB.HasProperty("hiddenPropertyMethod"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdB.SetProperty("hiddenPropertyMethod") + if err != nil { + t.Fatal(err) + } + if exp, got := true, b.AHiddenPMFlag; exp != got { + t.Fatalf("unexpected b.AHiddenPMFlag got: %v exp: %v", got, exp) + } + + // Test B.HiddenChainMethod as chain + if exp, got := true, rdB.HasChainMethod("hiddenChainMethod"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdB.CallChainMethod("hiddenChainMethod") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, b.A.hcmCallCount; exp != got { + t.Fatalf("unexpected b.A.hcmCallCount got: %v exp: %v", got, exp) + } + + // Test B.HiddenPropertyMethod as property + if exp, got := true, rdB.HasProperty("hiddenChainMethod"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdB.SetProperty("hiddenChainMethod", "test") + if err != nil { + t.Fatal(err) + } + if exp, got := "test", b.BOverriddingProp; exp != got { + t.Fatalf("unexpected b.BOverriddingProp got: %v exp: %v", got, exp) + } + + // Test B.ChainMethodA as chain + if exp, got := true, rdB.HasChainMethod("chainMethodA"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdB.CallChainMethod("chainMethodA") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, b.A.chainMethodCallCount; exp != got { + t.Fatalf("unexpected b.A.chainMethodCallCount got: %v exp: %v", got, exp) + } + + // Test B.ChainMethodA as property + if exp, got := true, rdB.HasProperty("chainMethodA"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdB.SetProperty("chainMethodA", "test") + if err != nil { + t.Fatal(err) + } + if exp, got := "test", b.ChainMethodA; exp != got { + t.Fatalf("unexpected b.ChainMethodA got: %v exp: %v", got, exp) + } + + // Test B.BProperty + if exp, got := true, rdB.HasProperty("bProperty"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + if exp, got := false, rdB.HasChainMethod("bProperty"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdB.SetProperty("bProperty", "test") + if err != nil { + t.Fatal(err) + } + if exp, got := "test", b.BProperty; exp != got { + t.Fatalf("unexpected b.BProperty got: %v exp: %v", got, exp) + } + + // Test B.PropertyMethodB + if exp, got := true, rdB.HasProperty("propertyMethodB"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + if exp, got := false, rdB.HasChainMethod("propertyMethodB"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdB.SetProperty("propertyMethodB") + if err != nil { + t.Fatal(err) + } + if exp, got := true, b.BFlag; exp != got { + t.Fatalf("unexpected b.BFlag got: %v exp: %v", got, exp) + } + if exp, got := 1, b.propertyMethodCallCount; exp != got { + t.Fatalf("unexpected b.propertyMethodCallCount got: %v exp: %v", got, exp) + } + + // Test B.ChainMethodB + if exp, got := true, rdB.HasChainMethod("chainMethodB"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + if exp, got := false, rdB.HasProperty("chainMethodB"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdB.CallChainMethod("chainMethodB") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, b.chainMethodCallCount; exp != got { + t.Fatalf("unexpected b.chainMethodCallCount got: %v exp: %v", got, exp) + } + + //---------------- + // Test C type + // + c := &C{ + A: new(A), + } + rdC, err := tick.NewReflectionDescriber(c, map[string]reflect.Value{ + "HiddenPropertyMethod": reflect.ValueOf(c.HiddenPropertyMethod), + "HiddenChainMethod": reflect.ValueOf(c.A.HiddenChainMethod), + }) + if err != nil { + t.Fatal(err) + } + + // Test C.AProperty as property + if exp, got := true, rdC.HasProperty("aProperty"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdC.SetProperty("aProperty", "test") + if err != nil { + t.Fatal(err) + } + if exp, got := "test", c.A.AProperty; exp != got { + t.Fatalf("unexpected c.A.AProperty got: %v exp: %v", got, exp) + } + + // Test C.AProperty as chain + if exp, got := true, rdC.HasChainMethod("aProperty"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdC.CallChainMethod("aProperty") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, c.apCallCount; exp != got { + t.Fatalf("unexpected c.apCallCount got: %v exp: %v", got, exp) + } + + // Test C.PropertyMethodA + if exp, got := true, rdC.HasProperty("propertyMethodA"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + if exp, got := false, rdC.HasChainMethod("propertyMethodA"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + if exp, got := false, rdC.HasProperty("aFlag"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdC.SetProperty("propertyMethodA") + if err != nil { + t.Fatal(err) + } + if exp, got := true, c.AFlag; exp != got { + t.Fatalf("unexpected c.AFlag got: %v exp: %v", got, exp) + } + if exp, got := 1, c.A.propertyMethodCallCount; exp != got { + t.Fatalf("unexpected c.A.propertyMethodCallCount got: %v exp: %v", got, exp) + } + + // Test C.HiddenPropertyMethod as chain + if exp, got := true, rdC.HasChainMethod("hiddenPropertyMethod"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdC.CallChainMethod("hiddenPropertyMethod") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, c.hpmCallCount; exp != got { + t.Fatalf("unexpected c.hpmCallCount got: %v exp: %v", got, exp) + } + + // Test C.HiddenPropertyMethod as property + if exp, got := true, rdC.HasProperty("hiddenPropertyMethod"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdC.SetProperty("hiddenPropertyMethod") + if err != nil { + t.Fatal(err) + } + if exp, got := true, c.AHiddenPMFlag; exp != got { + t.Fatalf("unexpected c.AHiddenPMFlag got: %v exp: %v", got, exp) + } + + // Test C.HiddenChainMethod as chain + if exp, got := true, rdC.HasChainMethod("hiddenChainMethod"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdC.CallChainMethod("hiddenChainMethod") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, c.A.hcmCallCount; exp != got { + t.Fatalf("unexpected c.A.hcmCallCount got: %v exp: %v", got, exp) + } + + // Test C.HiddenPropertyMethod as property + if exp, got := true, rdC.HasProperty("hiddenChainMethod"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdC.SetProperty("hiddenChainMethod", "test") + if err != nil { + t.Fatal(err) + } + if exp, got := "test", c.COverriddingProp; exp != got { + t.Fatalf("unexpected c.COverriddingProp got: %v exp: %v", got, exp) + } + + // Test C.ChainMethodA as chain + if exp, got := true, rdC.HasChainMethod("chainMethodA"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdC.CallChainMethod("chainMethodA") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, c.A.chainMethodCallCount; exp != got { + t.Fatalf("unexpected c.A.chainMethodCallCount got: %v exp: %v", got, exp) + } + + // Test C.ChainMethodA as property + if exp, got := true, rdC.HasProperty("chainMethodA"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdC.SetProperty("chainMethodA", "test") + if err != nil { + t.Fatal(err) + } + if exp, got := "test", c.ChainMethodA; exp != got { + t.Fatalf("unexpected c.ChainMethodA got: %v exp: %v", got, exp) + } + + // Test C.CProperty + if exp, got := true, rdC.HasProperty("cProperty"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + if exp, got := false, rdC.HasChainMethod("cProperty"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdC.SetProperty("cProperty", "test") + if err != nil { + t.Fatal(err) + } + if exp, got := "test", c.CProperty; exp != got { + t.Fatalf("unexpected c.CProperty got: %v exp: %v", got, exp) + } + + // Test C.PropertyMethodC + if exp, got := true, rdC.HasProperty("propertyMethodC"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + if exp, got := false, rdC.HasChainMethod("propertyMethodC"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + _, err = rdC.SetProperty("propertyMethodC") + if err != nil { + t.Fatal(err) + } + if exp, got := true, c.CFlag; exp != got { + t.Fatalf("unexpected c.CFlag got: %v exp: %v", got, exp) + } + if exp, got := 1, c.propertyMethodCallCount; exp != got { + t.Fatalf("unexpected c.propertyMethodCallCount got: %v exp: %v", got, exp) + } + + // Test C.ChainMethodC + if exp, got := true, rdC.HasChainMethod("chainMethodC"); exp != got { + t.Fatalf("unexpected HasChainMethod got: %v exp: %v", got, exp) + } + if exp, got := false, rdC.HasProperty("chainMethodC"); exp != got { + t.Fatalf("unexpected HasProperty got: %v exp: %v", got, exp) + } + _, err = rdC.CallChainMethod("chainMethodC") + if err != nil { + t.Fatal(err) + } + if exp, got := 1, c.chainMethodCallCount; exp != got { + t.Fatalf("unexpected c.chainMethodCallCount got: %v exp: %v", got, exp) + } +} + +func TestReflectionDescriberErrors(t *testing.T) { + _, err := tick.NewReflectionDescriber(nil, nil) + if err == nil { + t.Error("expected err got nil") + } + + o := struct{}{} + _, err = tick.NewReflectionDescriber(o, nil) + if err == nil { + t.Error("expected err got nil") + } + + var c *C + _, err = tick.NewReflectionDescriber(c, nil) + if err == nil { + t.Error("expected err got nil") + } + + c = new(C) + _, err = tick.NewReflectionDescriber(c, nil) + if err == nil { + t.Error("expected err got nil") + } + + i := new(int) + *i = 42 + _, err = tick.NewReflectionDescriber(i, nil) + if err == nil { + t.Error("expected err got nil") + } + +}