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 }