add existence func and unit test

This commit is contained in:
Daniel Smith 2014-12-23 14:05:26 -08:00
parent 16c624b2e6
commit 7f46d420dd
4 changed files with 406 additions and 6 deletions

View File

@ -332,7 +332,9 @@ type TemplatePrinter struct {
}
func NewTemplatePrinter(tmpl []byte) (*TemplatePrinter, error) {
t, err := template.New("output").Parse(string(tmpl))
t, err := template.New("output").
Funcs(template.FuncMap{"exists": exists}).
Parse(string(tmpl))
if err != nil {
return nil, err
}
@ -346,7 +348,7 @@ func (t *TemplatePrinter) Print(data []byte, w io.Writer) error {
if err != nil {
return err
}
if err := t.template.Execute(w, out); err != nil {
if err := t.safeExecute(w, out); err != nil {
// It is way easier to debug this stuff when it shows up in
// stdout instead of just stdin. So in addition to returning
// a nice error, also print useful stuff with the writer.
@ -367,3 +369,93 @@ func (t *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
}
return t.Print(data, w)
}
// safeExecute tries to execute the template, but catches panics and returns an error
// should the template engine panic.
func (p *TemplatePrinter) safeExecute(w io.Writer, obj interface{}) error {
var panicErr error
// Sorry for the double anonymous function. There's probably a clever way
// to do this that has the defer'd func setting the value to be returned, but
// that would be even less obvious.
retErr := func() error {
defer func() {
if x := recover(); x != nil {
panicErr = fmt.Errorf("caught panic: %+v", x)
}
}()
return p.template.Execute(w, obj)
}()
if panicErr != nil {
return panicErr
}
return retErr
}
// exists returns true if it would be possible to call the index function
// with these arguments.
//
// TODO: how to document this for users?
//
// index returns the result of indexing its first argument by the following
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
// indexed item must be a map, slice, or array.
func exists(item interface{}, indices ...interface{}) bool {
v := reflect.ValueOf(item)
for _, i := range indices {
index := reflect.ValueOf(i)
var isNil bool
if v, isNil = indirect(v); isNil {
return false
}
switch v.Kind() {
case reflect.Array, reflect.Slice, reflect.String:
var x int64
switch index.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x = index.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
x = int64(index.Uint())
default:
return false
}
if x < 0 || x >= int64(v.Len()) {
return false
}
v = v.Index(int(x))
case reflect.Map:
if !index.IsValid() {
index = reflect.Zero(v.Type().Key())
}
if !index.Type().AssignableTo(v.Type().Key()) {
return false
}
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
default:
return false
}
}
if _, isNil := indirect(v); isNil {
return false
}
return true
}
// stolen from text/template
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
// We indirect through pointers and empty interfaces (only) because
// non-empty interfaces have methods we might need.
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
break
}
}
return v, false
}

View File

@ -26,6 +26,8 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/ghodss/yaml"
)
@ -180,7 +182,6 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) {
func TestTemplatePanic(t *testing.T) {
tmpl := `{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}`
// kind is always blank in memory and set on the wire
printer, err := NewTemplatePrinter([]byte(tmpl))
if err != nil {
t.Fatalf("tmpl fail: %v", err)
@ -194,3 +195,111 @@ func TestTemplatePanic(t *testing.T) {
t.Errorf("no debugging info was printed")
}
}
func TestTemplateStrings(t *testing.T) {
// This unit tests the "exists" function as well as the template from update.sh
table := map[string]struct {
pod api.Pod
expect string
}{
"nilInfo": {api.Pod{}, "false"},
"emptyInfo": {api.Pod{Status: api.PodStatus{Info: api.PodInfo{}}}, "false"},
"containerExists": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{"update-demo": api.ContainerStatus{}},
},
},
"false",
},
"netExists": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{"net": api.ContainerStatus{}},
},
},
"false",
},
"bothExist": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{
"update-demo": api.ContainerStatus{},
"net": api.ContainerStatus{},
},
},
},
"false",
},
"oneValid": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{
"update-demo": api.ContainerStatus{},
"net": api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{
StartedAt: util.Time{},
},
},
},
},
},
},
"false",
},
"bothValid": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{
"update-demo": api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{
StartedAt: util.Time{},
},
},
},
"net": api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{
StartedAt: util.Time{},
},
},
},
},
},
},
"true",
},
}
// The point of this test is to verify that the below template works. If you change this
// template, you need to update hack/e2e-suite/update.sh.
tmpl :=
`{{and (exists . "currentState" "info" "update-demo" "state" "running") (exists . "currentState" "info" "net" "state" "running")}}`
useThisToDebug := `
a: {{exists . "currentState"}}
b: {{exists . "currentState" "info"}}
c: {{exists . "currentState" "info" "update-demo"}}
d: {{exists . "currentState" "info" "update-demo" "state"}}
e: {{exists . "currentState" "info" "update-demo" "state" "running"}}
f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"}}`
_ = useThisToDebug // don't complain about unused var
printer, err := NewTemplatePrinter([]byte(tmpl))
if err != nil {
t.Fatalf("tmpl fail: %v", err)
}
for name, item := range table {
buffer := &bytes.Buffer{}
err = printer.PrintObj(&item.pod, buffer)
if err != nil {
t.Errorf("%v: unexpected err: %v", name, err)
continue
}
if e, a := item.expect, buffer.String(); e != a {
t.Errorf("%v: expected %v, got %v", name, e, a)
}
}
}

View File

@ -390,7 +390,9 @@ type TemplatePrinter struct {
}
func NewTemplatePrinter(tmpl []byte, asVersion string, convertor runtime.ObjectConvertor) (*TemplatePrinter, error) {
t, err := template.New("output").Parse(string(tmpl))
t, err := template.New("output").
Funcs(template.FuncMap{"exists": exists}).
Parse(string(tmpl))
if err != nil {
return nil, err
}
@ -416,7 +418,7 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
if err := json.Unmarshal(data, &out); err != nil {
return err
}
if err = p.template.Execute(w, out); err != nil {
if err = p.safeExecute(w, out); err != nil {
// It is way easier to debug this stuff when it shows up in
// stdout instead of just stdin. So in addition to returning
// a nice error, also print useful stuff with the writer.
@ -429,6 +431,27 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
return nil
}
// safeExecute tries to execute the template, but catches panics and returns an error
// should the template engine panic.
func (p *TemplatePrinter) safeExecute(w io.Writer, obj interface{}) error {
var panicErr error
// Sorry for the double anonymous function. There's probably a clever way
// to do this that has the defer'd func setting the value to be returned, but
// that would be even less obvious.
retErr := func() error {
defer func() {
if x := recover(); x != nil {
panicErr = fmt.Errorf("caught panic: %+v", x)
}
}()
return p.template.Execute(w, obj)
}()
if panicErr != nil {
return panicErr
}
return retErr
}
func tabbedString(f func(io.Writer) error) (string, error) {
out := new(tabwriter.Writer)
buf := &bytes.Buffer{}
@ -443,3 +466,72 @@ func tabbedString(f func(io.Writer) error) (string, error) {
str := string(buf.String())
return str, nil
}
// exists returns true if it would be possible to call the index function
// with these arguments.
//
// TODO: how to document this for users?
//
// index returns the result of indexing its first argument by the following
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
// indexed item must be a map, slice, or array.
func exists(item interface{}, indices ...interface{}) bool {
v := reflect.ValueOf(item)
for _, i := range indices {
index := reflect.ValueOf(i)
var isNil bool
if v, isNil = indirect(v); isNil {
return false
}
switch v.Kind() {
case reflect.Array, reflect.Slice, reflect.String:
var x int64
switch index.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x = index.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
x = int64(index.Uint())
default:
return false
}
if x < 0 || x >= int64(v.Len()) {
return false
}
v = v.Index(int(x))
case reflect.Map:
if !index.IsValid() {
index = reflect.Zero(v.Type().Key())
}
if !index.Type().AssignableTo(v.Type().Key()) {
return false
}
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
default:
return false
}
}
if _, isNil := indirect(v); isNil {
return false
}
return true
}
// stolen from text/template
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
// We indirect through pointers and empty interfaces (only) because
// non-empty interfaces have methods we might need.
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
break
}
}
return v, false
}

View File

@ -292,7 +292,6 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) {
func TestTemplatePanic(t *testing.T) {
tmpl := `{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}`
// kind is always blank in memory and set on the wire
printer, err := NewTemplatePrinter([]byte(tmpl), testapi.Version(), api.Scheme)
if err != nil {
t.Fatalf("tmpl fail: %v", err)
@ -307,6 +306,114 @@ func TestTemplatePanic(t *testing.T) {
}
}
func TestTemplateStrings(t *testing.T) {
// This unit tests the "exists" function as well as the template from update.sh
table := map[string]struct {
pod api.Pod
expect string
}{
"nilInfo": {api.Pod{}, "false"},
"emptyInfo": {api.Pod{Status: api.PodStatus{Info: api.PodInfo{}}}, "false"},
"containerExists": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{"update-demo": api.ContainerStatus{}},
},
},
"false",
},
"netExists": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{"net": api.ContainerStatus{}},
},
},
"false",
},
"bothExist": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{
"update-demo": api.ContainerStatus{},
"net": api.ContainerStatus{},
},
},
},
"false",
},
"oneValid": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{
"update-demo": api.ContainerStatus{},
"net": api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{
StartedAt: util.Time{},
},
},
},
},
},
},
"false",
},
"bothValid": {
api.Pod{
Status: api.PodStatus{
Info: api.PodInfo{
"update-demo": api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{
StartedAt: util.Time{},
},
},
},
"net": api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{
StartedAt: util.Time{},
},
},
},
},
},
},
"true",
},
}
// The point of this test is to verify that the below template works. If you change this
// template, you need to update hack/e2e-suite/update.sh.
tmpl :=
`{{and (exists . "currentState" "info" "update-demo" "state" "running") (exists . "currentState" "info" "net" "state" "running")}}`
useThisToDebug := `
a: {{exists . "currentState"}}
b: {{exists . "currentState" "info"}}
c: {{exists . "currentState" "info" "update-demo"}}
d: {{exists . "currentState" "info" "update-demo" "state"}}
e: {{exists . "currentState" "info" "update-demo" "state" "running"}}
f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"}}`
_ = useThisToDebug // don't complain about unused var
printer, err := NewTemplatePrinter([]byte(tmpl), "v1beta1", api.Scheme)
if err != nil {
t.Fatalf("tmpl fail: %v", err)
}
for name, item := range table {
buffer := &bytes.Buffer{}
err = printer.PrintObj(&item.pod, buffer)
if err != nil {
t.Errorf("%v: unexpected err: %v", name, err)
continue
}
if e, a := item.expect, buffer.String(); e != a {
t.Errorf("%v: expected %v, got %v", name, e, a)
}
}
}
func TestPrinters(t *testing.T) {
om := func(name string) api.ObjectMeta { return api.ObjectMeta{Name: name} }
templatePrinter, err := NewTemplatePrinter([]byte("{{.name}}"), testapi.Version(), api.Scheme)