rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package gallery
2
3import (
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.
25var 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.
43var 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.
51var 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
56var 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.
61func 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.
81const proseExporter = "../../hack/seed/_rastrillo/prose_fiestaseed_test.go"
82
83// proseRuleNames are the declarations that make up the rule, in both
84// files.
85var 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.
90func 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
119func 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.
136func 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