1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package main
16
17 import (
18 "bytes"
19 "flag"
20 "fmt"
21 "go/ast"
22 "go/constant"
23 "go/format"
24 "log"
25 "os"
26 "path/filepath"
27 "strings"
28
29 "golang.org/x/tools/go/packages"
30 "golang.org/x/tools/txtar"
31
32 "cuelang.org/go/cue"
33 cueast "cuelang.org/go/cue/ast"
34 cueformat "cuelang.org/go/cue/format"
35 "cuelang.org/go/cue/parser"
36 "cuelang.org/go/internal"
37 internaljson "cuelang.org/go/internal/encoding/json"
38 "cuelang.org/go/pkg/encoding/yaml"
39 "cuelang.org/go/tools/fix"
40 )
41
42
43
44
45
46 func main() {
47 flag.Parse()
48 log.SetFlags(log.Lshortfile)
49
50 cfg := &packages.Config{
51 Mode: packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo,
52
53 Tests: true,
54 }
55 a, err := packages.Load(cfg, "..")
56 if err != nil {
57 log.Fatal(err)
58 }
59
60 for _, p := range a {
61 e := extractor{p: p}
62 e.extractFromPackage()
63 }
64 }
65
66 type extractor struct {
67 p *packages.Package
68
69 dir string
70 name string
71 count int
72 a *txtar.Archive
73 header *bytes.Buffer
74
75 tags []string
76 }
77
78 func (e *extractor) fatalf(format string, args ...interface{}) {
79 prefix := fmt.Sprintf("%s/%d[%s]: ", e.dir, e.count, e.name)
80 log.Fatalf(prefix+format, args...)
81 }
82
83 func (e *extractor) warnf(format string, args ...interface{}) {
84 prefix := fmt.Sprintf("%s/%d[%s]: ", e.dir, e.count, e.name)
85 log.Printf(prefix+format, args...)
86 }
87
88 func (e *extractor) extractFromPackage() {
89 for _, file := range e.p.Syntax {
90 for _, d := range file.Decls {
91 e.processTestFunc(d)
92 }
93 }
94 }
95
96 func (e *extractor) processTestFunc(d ast.Decl) {
97 p := e.p
98 f, ok := d.(*ast.FuncDecl)
99 if !ok {
100 return
101 }
102
103 if !strings.HasPrefix(f.Name.Name, "Test") {
104 return
105 }
106 e.dir = strings.ToLower(f.Name.Name[len("Test"):])
107 e.count = 0
108 e.tags = nil
109 if e.dir == "x" {
110 return
111 }
112
113 if len(f.Type.Params.List) != 1 {
114 return
115 }
116
117 if p.TypesInfo.TypeOf(f.Type.Params.List[0].Type).String() != "*testing.T" {
118 return
119 }
120 e.extractFromTestFunc(f)
121 }
122
123 func (e *extractor) isConstant(x ast.Expr) bool {
124 return constant.Val(e.p.TypesInfo.Types[x].Value) != nil
125 }
126
127 func (e *extractor) stringConst(x ast.Expr) string {
128 v := e.p.TypesInfo.Types[x].Value
129 if v.Kind() != constant.String {
130 return v.String()
131 }
132 return constant.StringVal(v)
133 }
134
135 func (e *extractor) boolConst(x ast.Expr) bool {
136 v := e.p.TypesInfo.Types[x].Value
137 return constant.BoolVal(v)
138 }
139
140 func (e *extractor) exprString(x ast.Expr) string {
141 w := &bytes.Buffer{}
142 _ = format.Node(w, e.p.Fset, x)
143 return w.String()
144 }
145
146 func (e *extractor) extractFromTestFunc(f *ast.FuncDecl) {
147 defer func() {
148 if err := recover(); err != nil {
149 e.warnf("PANIC: %v", err)
150 panic(err)
151 }
152 }()
153
154 for _, stmt := range f.Body.List {
155 es, ok := stmt.(*ast.ExprStmt)
156 if !ok {
157 continue
158 }
159 if call, ok := es.X.(*ast.CallExpr); ok {
160 if e.exprString(call.Fun) != "rewriteHelper" {
161 continue
162 }
163 e.tags = append(e.tags, e.exprString(call.Args[2]))
164 }
165 }
166
167
168 for _, stmt := range f.Body.List {
169 ast.Inspect(stmt, func(n ast.Node) bool {
170 switch x := n.(type) {
171 case *ast.CompositeLit:
172 t := e.p.TypesInfo.TypeOf(x.Type)
173
174 switch t.String() {
175 case "[]cuelang.org/go/cue.testCase",
176 "[]cuelang.org/go/cue.exportTest":
177
178 default:
179 return false
180 }
181
182 for _, elt := range x.Elts {
183 if kv, ok := elt.(*ast.KeyValueExpr); ok {
184 elt = kv.Value
185 }
186 e.extractTest(elt.(*ast.CompositeLit))
187 e.count++
188 }
189
190 return false
191 }
192 return true
193 })
194 }
195 }
196
197 func (e *extractor) extractTest(x *ast.CompositeLit) {
198 e.name = ""
199 e.header = &bytes.Buffer{}
200 e.a = &txtar.Archive{}
201
202 e.header.WriteString(`# DO NOT EDIT; generated by go run testdata/gen.go
203 #
204 `)
205
206 for _, elmt := range x.Elts {
207 f, ok := elmt.(*ast.KeyValueExpr)
208 if !ok {
209 e.fatalf("Invalid slice element: %T", elmt)
210 continue
211 }
212
213 switch key := e.exprString(f.Key); key {
214 case "name", "desc":
215 e.name = e.stringConst(f.Value)
216 fmt.Fprintf(e.header, "#name: %v\n", e.stringConst(f.Value))
217
218 case "in":
219 src := []byte(e.stringConst(f.Value))
220 src, err := cueformat.Source(src)
221
222 if f, err := parser.ParseFile("in.cue", src, parser.ParseComments); err == nil {
223 f = fix.File(f)
224 b, err := cueformat.Node(f)
225 if err == nil {
226 src = b
227 }
228 }
229
230 if err != nil {
231 fmt.Fprintln(e.header, "#skip")
232 e.warnf("Skipped: %v", err)
233 continue
234 }
235 e.a.Files = append(e.a.Files, txtar.File{
236 Name: "in.cue",
237 Data: src,
238 })
239
240 e.populate(src)
241
242 case "out":
243 if !e.isConstant(f.Value) {
244 e.warnf("Could not determine value for 'out' field")
245 continue
246 }
247 e.a.Files = append(e.a.Files, txtar.File{
248 Name: "out/legacy-debug",
249 Data: []byte(e.stringConst(f.Value)),
250 })
251 default:
252 fmt.Fprintf(e.header, "%s: %v\n", key, e.exprString(f.Value))
253 }
254 }
255
256 for _, t := range e.tags {
257 fmt.Fprintf(e.header, "#%s\n", t)
258 }
259
260 e.a.Comment = e.header.Bytes()
261
262 _ = os.Mkdir(e.dir, 0755)
263
264 name := fmt.Sprintf("%03d", e.count)
265 if e.name != "" {
266 name += "_" + e.name
267 }
268 name = strings.ReplaceAll(name, " ", "_")
269 name = strings.ReplaceAll(name, ":", "_")
270 filename := filepath.Join(e.dir, name+".txtar")
271 err := os.WriteFile(filename, txtar.Format(e.a), 0644)
272 if err != nil {
273 e.fatalf("Could not write file: %v", err)
274 }
275 }
276
277
278
279 func (e *extractor) populate(src []byte) {
280 r := cue.Runtime{}
281 inst, err := r.Compile("in.cue", src)
282 if err != nil {
283 e.fatalf("Failed to parse: %v", err)
284 }
285
286 v := inst.Value()
287
288 e.addFile(e.a, "out/def", v.Syntax(
289 cue.Docs(true),
290 cue.Attributes(true),
291 cue.Optional(true),
292 cue.Definitions(true)))
293
294 if v.Validate(cue.Concrete(true)) == nil {
295 e.addFile(e.a, "out/export", v.Syntax(
296 cue.Concrete(true),
297 cue.Final(),
298 cue.Docs(true),
299 cue.Attributes(true)))
300
301 s, err := yaml.Marshal(v)
302 if err != nil {
303 fmt.Fprintln(e.header, "#bug: true")
304 e.warnf("Could not encode as YAML: %v", err)
305 }
306 e.a.Files = append(e.a.Files,
307 txtar.File{Name: "out/yaml", Data: []byte(s)})
308
309 b, err := internaljson.Marshal(v)
310 if err != nil {
311 fmt.Fprintln(e.header, "#bug: true")
312 e.warnf("Could not encode as JSON: %v", err)
313 }
314 e.a.Files = append(e.a.Files,
315 txtar.File{Name: "out/json", Data: b})
316 }
317 }
318
319 func (e *extractor) addFile(a *txtar.Archive, name string, n cueast.Node) {
320 b, err := cueformat.Node(internal.ToFile(n))
321 if err != nil {
322 e.fatalf("Failed to format %s: %v\n", name, err)
323 }
324 a.Files = append(a.Files, txtar.File{Name: name, Data: b})
325 }
326
View as plain text