| 1 | package gallery |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "go/ast" |
| 6 | "go/parser" |
| 7 | "go/printer" |
| 8 | "go/token" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "regexp" |
| 12 | "testing" |
| 13 | ) |
| 14 | |
| 15 | // The review rule the prose exporter applies to rastrillo's strings, |
| 16 | // copied here so the same rule reads prose.json. The exporter, |
| 17 | // hack/seed/_rastrillo/prose_fiestaseed_test.go, runs inside a |
| 18 | // rastrillo worktree and cannot be imported, so the mechanical part |
| 19 | // of its rule is the same text in both files, and |
| 20 | // TestTheProseRuleIsTheExporters fails when the two drift. Change it |
| 21 | // there and copy it here. |
| 22 | |
| 23 | // proseWords are the words that put a string in the review queue: |
| 24 | // each names something Fiesta's HTML does not have. |
| 25 | var proseWords = []struct { |
| 26 | what string |
| 27 | re *regexp.Regexp |
| 28 | }{ |
| 29 | {"Go", regexp.MustCompile(`\bGo\b`)}, |
| 30 | {"a template", regexp.MustCompile(`(?i)\btemplates?\b`)}, |
| 31 | {"{{", regexp.MustCompile(`\{\{`)}, |
| 32 | {"ui.", regexp.MustCompile(`\bui\.`)}, |
| 33 | {"rastrillo", regexp.MustCompile(`(?i)rastrillo`)}, |
| 34 | {"a partial", regexp.MustCompile(`(?i)\bpartials?\b`)}, |
| 35 | {"/_theme", regexp.MustCompile(`/_theme`)}, |
| 36 | {"a cookie", regexp.MustCompile(`(?i)\bcookies?\b`)}, |
| 37 | {"a server", regexp.MustCompile(`(?i)\bservers?\b`)}, |
| 38 | {".go", regexp.MustCompile(`\.go\b`)}, |
| 39 | } |
| 40 | |
| 41 | // proseFields are the capitalised field names a note uses as a |
| 42 | // template argument. |
| 43 | var proseFields = []string{ |
| 44 | "Lead", "Exact", "Primary", "Hidden", "ClearHref", "Placeholder", "Fit", "Desc", |
| 45 | "SelectName", "CancelLabel", "Alert", "Tone", "Options", "Plain", "RevokePath", |
| 46 | "RevokeAgentPath", "RevokeAllPath", "Created", "Currency", "Render", "PageData", |
| 47 | } |
| 48 | |
| 49 | // proseScripts are the script files a string may name: the ones |
| 50 | // Fiesta ships under that name. |
| 51 | var proseScripts = map[string]bool{ |
| 52 | "anchor.js": true, "busy.js": true, "calendar.js": true, "copy.js": true, "datetime.js": true, |
| 53 | "fiesta.js": true, "light.js": true, "select.js": true, "shell.js": true, |
| 54 | } |
| 55 | |
| 56 | var proseScript = regexp.MustCompile(`\b[a-z][a-z0-9-]*\.js\b`) |
| 57 | |
| 58 | // proseReason is the mechanical part of the review: why text cannot |
| 59 | // carry, or "" when the word list, the template arguments and the |
| 60 | // script names all pass. |
| 61 | func proseReason(text string) string { |
| 62 | for _, w := range proseWords { |
| 63 | if w.re.MatchString(text) { |
| 64 | return "mentions " + w.what |
| 65 | } |
| 66 | } |
| 67 | for _, f := range proseFields { |
| 68 | if regexp.MustCompile(`\b` + f + `\b`).MatchString(text) { |
| 69 | return "uses the template argument " + f |
| 70 | } |
| 71 | } |
| 72 | for _, s := range proseScript.FindAllString(text, -1) { |
| 73 | if !proseScripts[s] { |
| 74 | return "names " + s + ", which Fiesta does not ship" |
| 75 | } |
| 76 | } |
| 77 | return "" |
| 78 | } |
| 79 | |
| 80 | // The exporter's file, from this package's directory. |
| 81 | const proseExporter = "../../hack/seed/_rastrillo/prose_fiestaseed_test.go" |
| 82 | |
| 83 | // proseRuleNames are the declarations that make up the rule, in both |
| 84 | // files. |
| 85 | var proseRuleNames = []string{"proseWords", "proseFields", "proseScripts", "proseScript", "proseReason"} |
| 86 | |
| 87 | // declText is the source of one top-level declaration in a parsed |
| 88 | // file, printed without its comments, so the two copies may be |
| 89 | // commented differently and must otherwise read the same. |
| 90 | func declText(t *testing.T, fset *token.FileSet, f *ast.File, name string) string { |
| 91 | t.Helper() |
| 92 | for _, d := range f.Decls { |
| 93 | var n ast.Node |
| 94 | switch d := d.(type) { |
| 95 | case *ast.FuncDecl: |
| 96 | if d.Name.Name == name { |
| 97 | n = &ast.FuncDecl{Recv: d.Recv, Name: d.Name, Type: d.Type, Body: d.Body} |
| 98 | } |
| 99 | case *ast.GenDecl: |
| 100 | for _, s := range d.Specs { |
| 101 | if v, ok := s.(*ast.ValueSpec); ok && len(v.Names) == 1 && v.Names[0].Name == name { |
| 102 | n = &ast.ValueSpec{Names: v.Names, Type: v.Type, Values: v.Values} |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | if n == nil { |
| 107 | continue |
| 108 | } |
| 109 | var b bytes.Buffer |
| 110 | if err := printer.Fprint(&b, fset, n); err != nil { |
| 111 | t.Fatal(err) |
| 112 | } |
| 113 | return b.String() |
| 114 | } |
| 115 | t.Fatalf("%s: no declaration named %s", fset.Position(f.Pos()).Filename, name) |
| 116 | return "" |
| 117 | } |
| 118 | |
| 119 | func parseFile(t *testing.T, fset *token.FileSet, path string) *ast.File { |
| 120 | t.Helper() |
| 121 | src, err := os.ReadFile(path) |
| 122 | if err != nil { |
| 123 | t.Fatal(err) |
| 124 | } |
| 125 | f, err := parser.ParseFile(fset, path, src, parser.ParseComments|parser.SkipObjectResolution) |
| 126 | if err != nil { |
| 127 | t.Fatal(err) |
| 128 | } |
| 129 | return f |
| 130 | } |
| 131 | |
| 132 | // TestTheProseRuleIsTheExporters: the five declarations above are, |
| 133 | // comments aside, the exporter's own, so the rule prose.json is read |
| 134 | // by is the rule its strings were queued by. The control is a copy |
| 135 | // of this file with one word added to the field list. |
| 136 | func TestTheProseRuleIsTheExporters(t *testing.T) { |
| 137 | fset := token.NewFileSet() |
| 138 | here := parseFile(t, fset, "prose_rule_test.go") |
| 139 | there := parseFile(t, fset, filepath.FromSlash(proseExporter)) |
| 140 | for _, name := range proseRuleNames { |
| 141 | if a, b := declText(t, fset, here, name), declText(t, fset, there, name); a != b { |
| 142 | t.Errorf("%s differs from the exporter's:\n%s\n\nthe exporter has:\n%s", name, a, b) |
| 143 | } |
| 144 | } |
| 145 | src, err := os.ReadFile("prose_rule_test.go") |
| 146 | if err != nil { |
| 147 | t.Fatal(err) |
| 148 | } |
| 149 | drifted := bytes.Replace(src, []byte(`"Lead", "Exact",`), []byte(`"Lead", "Exact", "Drift",`), 1) |
| 150 | if bytes.Equal(drifted, src) { |
| 151 | t.Fatal("the control did not change the field list") |
| 152 | } |
| 153 | control, err := parser.ParseFile(fset, "control.go", drifted, parser.SkipObjectResolution) |
| 154 | if err != nil { |
| 155 | t.Fatal(err) |
| 156 | } |
| 157 | if declText(t, fset, control, "proseFields") == declText(t, fset, there, "proseFields") { |
| 158 | t.Fatal("the control adds a field and the comparison did not see it") |
| 159 | } |
| 160 | } |
| 161 | |