package httpd
import (
"html/template"
"reflect"
"testing"
"text/template/parse"
)
// A branch nobody renders hides a field nobody supplies, which published a private repository once.
func TestEveryTemplateFieldExistsOnItsData(t *testing.T) {
for name, c := range renderCases() {
page, ok := pages[name]
if !ok {
continue
}
// The walk starts at layout, because a page file is only its define blocks.
layout := page.Lookup("layout")
if layout == nil {
t.Errorf("%s has no layout", name)
continue
}
typ := reflect.TypeOf(c.data)
for _, field := range topLevelFields(layout.Tree, page) {
if !hasField(typ, field) {
t.Errorf("%s asks for .%s and its data has no such field, so a branch that "+
"reaches it renders nothing and says nothing", name, field)
}
}
}
}
// topLevelFields names every field read against the page's own dot, so range bodies are left alone.
func topLevelFields(tree *parse.Tree, page *template.Template) []string {
if tree == nil {
return nil
}
var out []string
var walk func(parse.Node)
walk = func(n parse.Node) {
switch t := n.(type) {
case nil:
return
case *parse.ListNode:
if t == nil {
return
}
for _, c := range t.Nodes {
walk(c)
}
case *parse.ActionNode:
walk(t.Pipe)
case *parse.IfNode:
walk(t.Pipe)
walk(t.List)
walk(t.ElseList)
case *parse.PipeNode:
if t == nil {
return
}
for _, c := range t.Cmds {
walk(c)
}
case *parse.CommandNode:
for _, a := range t.Args {
walk(a)
}
case *parse.FieldNode:
if len(t.Ident) > 0 {
out = append(out, t.Ident[0])
}
case *parse.TemplateNode:
// Only follow a template invoked with the same dot, since another argument is another type.
if isDot(t.Pipe) {
if inner := page.Lookup(t.Name); inner != nil {
out = append(out, topLevelFields(inner.Tree, page)...)
}
}
}
}
walk(tree.Root)
return out
}
// isDot reports whether a template was invoked with the dot it was already looking at.
func isDot(p *parse.PipeNode) bool {
if p == nil || len(p.Cmds) != 1 || len(p.Cmds[0].Args) != 1 {
return false
}
_, ok := p.Cmds[0].Args[0].(*parse.DotNode)
return ok
}
// hasField looks through embedded structs, which is how every page carries its chrome.
func hasField(typ reflect.Type, name string) bool {
if typ == nil {
return false
}
for typ.Kind() == reflect.Pointer {
typ = typ.Elem()
}
if typ.Kind() != reflect.Struct {
return false
}
if _, ok := typ.FieldByName(name); ok {
return true
}
return false
}