package gallery import ( "bytes" "go/ast" "go/parser" "go/printer" "go/token" "os" "path/filepath" "regexp" "testing" ) // The review rule the prose exporter applies to rastrillo's strings, // copied here so the same rule reads prose.json. The exporter, // hack/seed/_rastrillo/prose_fiestaseed_test.go, runs inside a // rastrillo worktree and cannot be imported, so the mechanical part // of its rule is the same text in both files, and // TestTheProseRuleIsTheExporters fails when the two drift. Change it // there and copy it here. // proseWords are the words that put a string in the review queue: // each names something Fiesta's HTML does not have. var proseWords = []struct { what string re *regexp.Regexp }{ {"Go", regexp.MustCompile(`\bGo\b`)}, {"a template", regexp.MustCompile(`(?i)\btemplates?\b`)}, {"{{", regexp.MustCompile(`\{\{`)}, {"ui.", regexp.MustCompile(`\bui\.`)}, {"rastrillo", regexp.MustCompile(`(?i)rastrillo`)}, {"a partial", regexp.MustCompile(`(?i)\bpartials?\b`)}, {"/_theme", regexp.MustCompile(`/_theme`)}, {"a cookie", regexp.MustCompile(`(?i)\bcookies?\b`)}, {"a server", regexp.MustCompile(`(?i)\bservers?\b`)}, {".go", regexp.MustCompile(`\.go\b`)}, } // proseFields are the capitalised field names a note uses as a // template argument. var proseFields = []string{ "Lead", "Exact", "Primary", "Hidden", "ClearHref", "Placeholder", "Fit", "Desc", "SelectName", "CancelLabel", "Alert", "Tone", "Options", "Plain", "RevokePath", "RevokeAgentPath", "RevokeAllPath", "Created", "Currency", "Render", "PageData", } // proseScripts are the script files a string may name: the ones // Fiesta ships under that name. var proseScripts = map[string]bool{ "anchor.js": true, "busy.js": true, "calendar.js": true, "copy.js": true, "datetime.js": true, "fiesta.js": true, "light.js": true, "select.js": true, "shell.js": true, } var proseScript = regexp.MustCompile(`\b[a-z][a-z0-9-]*\.js\b`) // proseReason is the mechanical part of the review: why text cannot // carry, or "" when the word list, the template arguments and the // script names all pass. func proseReason(text string) string { for _, w := range proseWords { if w.re.MatchString(text) { return "mentions " + w.what } } for _, f := range proseFields { if regexp.MustCompile(`\b` + f + `\b`).MatchString(text) { return "uses the template argument " + f } } for _, s := range proseScript.FindAllString(text, -1) { if !proseScripts[s] { return "names " + s + ", which Fiesta does not ship" } } return "" } // The exporter's file, from this package's directory. const proseExporter = "../../hack/seed/_rastrillo/prose_fiestaseed_test.go" // proseRuleNames are the declarations that make up the rule, in both // files. var proseRuleNames = []string{"proseWords", "proseFields", "proseScripts", "proseScript", "proseReason"} // declText is the source of one top-level declaration in a parsed // file, printed without its comments, so the two copies may be // commented differently and must otherwise read the same. func declText(t *testing.T, fset *token.FileSet, f *ast.File, name string) string { t.Helper() for _, d := range f.Decls { var n ast.Node switch d := d.(type) { case *ast.FuncDecl: if d.Name.Name == name { n = &ast.FuncDecl{Recv: d.Recv, Name: d.Name, Type: d.Type, Body: d.Body} } case *ast.GenDecl: for _, s := range d.Specs { if v, ok := s.(*ast.ValueSpec); ok && len(v.Names) == 1 && v.Names[0].Name == name { n = &ast.ValueSpec{Names: v.Names, Type: v.Type, Values: v.Values} } } } if n == nil { continue } var b bytes.Buffer if err := printer.Fprint(&b, fset, n); err != nil { t.Fatal(err) } return b.String() } t.Fatalf("%s: no declaration named %s", fset.Position(f.Pos()).Filename, name) return "" } func parseFile(t *testing.T, fset *token.FileSet, path string) *ast.File { t.Helper() src, err := os.ReadFile(path) if err != nil { t.Fatal(err) } f, err := parser.ParseFile(fset, path, src, parser.ParseComments|parser.SkipObjectResolution) if err != nil { t.Fatal(err) } return f } // TestTheProseRuleIsTheExporters: the five declarations above are, // comments aside, the exporter's own, so the rule prose.json is read // by is the rule its strings were queued by. The control is a copy // of this file with one word added to the field list. func TestTheProseRuleIsTheExporters(t *testing.T) { fset := token.NewFileSet() here := parseFile(t, fset, "prose_rule_test.go") there := parseFile(t, fset, filepath.FromSlash(proseExporter)) for _, name := range proseRuleNames { if a, b := declText(t, fset, here, name), declText(t, fset, there, name); a != b { t.Errorf("%s differs from the exporter's:\n%s\n\nthe exporter has:\n%s", name, a, b) } } src, err := os.ReadFile("prose_rule_test.go") if err != nil { t.Fatal(err) } drifted := bytes.Replace(src, []byte(`"Lead", "Exact",`), []byte(`"Lead", "Exact", "Drift",`), 1) if bytes.Equal(drifted, src) { t.Fatal("the control did not change the field list") } control, err := parser.ParseFile(fset, "control.go", drifted, parser.SkipObjectResolution) if err != nil { t.Fatal(err) } if declText(t, fset, control, "proseFields") == declText(t, fset, there, "proseFields") { t.Fatal("the control adds a field and the comparison did not see it") } }