blob: ef96b8280a22bd22ffcb42a4ff7115872c4b9d98 [file] [log] [blame]
// Copyright 2020 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"go/ast"
"go/constant"
"go/format"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/txtar"
"cuelang.org/go/cue"
cueast "cuelang.org/go/cue/ast"
cueformat "cuelang.org/go/cue/format"
"cuelang.org/go/cue/parser"
"cuelang.org/go/internal"
"cuelang.org/go/pkg/encoding/yaml"
"cuelang.org/go/tools/fix"
)
//go:generate go run gen.go
//go:generate go test ../internal/compile --update
//go:generate go test ../internal/eval --update
func main() {
flag.Parse()
log.SetFlags(log.Lshortfile)
cfg := &packages.Config{
Mode: packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo,
Tests: true,
}
a, err := packages.Load(cfg, "..")
if err != nil {
log.Fatal(err)
}
for _, p := range a {
e := extractor{p: p}
e.extractFromPackage()
}
}
type extractor struct {
p *packages.Package
dir string
name string
count int
a *txtar.Archive
header *bytes.Buffer
tags []string
}
func (e *extractor) fatalf(format string, args ...interface{}) {
prefix := fmt.Sprintf("%s/%d[%s]: ", e.dir, e.count, e.name)
log.Fatalf(prefix+format, args...)
}
func (e *extractor) warnf(format string, args ...interface{}) {
prefix := fmt.Sprintf("%s/%d[%s]: ", e.dir, e.count, e.name)
log.Printf(prefix+format, args...)
}
func (e *extractor) extractFromPackage() {
for _, file := range e.p.Syntax {
for _, d := range file.Decls {
e.processTestFunc(d)
}
}
}
func (e *extractor) processTestFunc(d ast.Decl) {
p := e.p
f, ok := d.(*ast.FuncDecl)
if !ok {
return
}
if !strings.HasPrefix(f.Name.Name, "Test") {
return
}
e.dir = strings.ToLower(f.Name.Name[len("Test"):])
e.count = 0
e.tags = nil
if e.dir == "x" { // TestX
return
}
if len(f.Type.Params.List) != 1 {
return
}
if p.TypesInfo.TypeOf(f.Type.Params.List[0].Type).String() != "*testing.T" {
return
}
e.extractFromTestFunc(f)
}
func (e *extractor) isConstant(x ast.Expr) bool {
return constant.Val(e.p.TypesInfo.Types[x].Value) != nil
}
func (e *extractor) stringConst(x ast.Expr) string {
v := e.p.TypesInfo.Types[x].Value
if v.Kind() != constant.String {
return v.String()
}
return constant.StringVal(v)
}
func (e *extractor) boolConst(x ast.Expr) bool {
v := e.p.TypesInfo.Types[x].Value
return constant.BoolVal(v)
}
func (e *extractor) exprString(x ast.Expr) string {
w := &bytes.Buffer{}
_ = format.Node(w, e.p.Fset, x)
return w.String()
}
func (e *extractor) extractFromTestFunc(f *ast.FuncDecl) {
defer func() {
if err := recover(); err != nil {
e.warnf("PANIC: %v", err)
panic(err)
}
}()
// Extract meta data.
for _, stmt := range f.Body.List {
es, ok := stmt.(*ast.ExprStmt)
if !ok {
continue
}
if call, ok := es.X.(*ast.CallExpr); ok {
if e.exprString(call.Fun) != "rewriteHelper" {
continue
}
e.tags = append(e.tags, e.exprString(call.Args[2]))
}
}
// Extract test data.
for _, stmt := range f.Body.List {
ast.Inspect(stmt, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.CompositeLit:
t := e.p.TypesInfo.TypeOf(x.Type)
switch t.String() {
case "[]cuelang.org/go/cue.testCase",
"[]cuelang.org/go/cue.exportTest":
// TODO: "[]cuelang.org/go/cue.subsumeTest",
default:
return false
}
for _, elt := range x.Elts {
if kv, ok := elt.(*ast.KeyValueExpr); ok {
elt = kv.Value
}
e.extractTest(elt.(*ast.CompositeLit))
e.count++
}
return false
}
return true
})
}
}
func (e *extractor) extractTest(x *ast.CompositeLit) {
e.name = ""
e.header = &bytes.Buffer{}
e.a = &txtar.Archive{}
e.header.WriteString(`# DO NOT EDIT; generated by go run testdata/gen.go
#
`)
for _, elmt := range x.Elts {
f, ok := elmt.(*ast.KeyValueExpr)
if !ok {
e.fatalf("Invalid slice element: %T", elmt)
continue
}
switch key := e.exprString(f.Key); key {
case "name", "desc":
e.name = e.stringConst(f.Value)
fmt.Fprintf(e.header, "#name: %v\n", e.stringConst(f.Value))
case "in":
src := []byte(e.stringConst(f.Value))
src, err := cueformat.Source(src)
if f, err := parser.ParseFile("in.cue", src, parser.ParseComments); err == nil {
f = fix.File(f)
b, err := cueformat.Node(f)
if err == nil {
src = b
}
}
if err != nil {
fmt.Fprintln(e.header, "#skip")
e.warnf("Skipped: %v", err)
continue
}
e.a.Files = append(e.a.Files, txtar.File{
Name: "in.cue",
Data: src,
})
e.populate(src)
case "out":
if !e.isConstant(f.Value) {
e.warnf("Could not determine value for 'out' field")
continue
}
e.a.Files = append(e.a.Files, txtar.File{
Name: "out/legacy-debug",
Data: []byte(e.stringConst(f.Value)),
})
default:
fmt.Fprintf(e.header, "%s: %v\n", key, e.exprString(f.Value))
}
}
for _, t := range e.tags {
fmt.Fprintf(e.header, "#%s\n", t)
}
e.a.Comment = e.header.Bytes()
_ = os.Mkdir(e.dir, 0755)
name := fmt.Sprintf("%03d", e.count)
if e.name != "" {
name += "_" + e.name
}
name = strings.ReplaceAll(name, " ", "_")
name = strings.ReplaceAll(name, ":", "_")
filename := filepath.Join(e.dir, name+".txtar")
err := ioutil.WriteFile(filename, txtar.Format(e.a), 0644)
if err != nil {
e.fatalf("Could not write file: %v", err)
}
}
// populate sets the golden tests based on the old compiler, evaluator,
// and exporter.
func (e *extractor) populate(src []byte) {
r := cue.Runtime{}
inst, err := r.Compile("in.cue", src)
if err != nil {
e.fatalf("Failed to parse: %v", err)
}
v := inst.Value()
e.addFile(e.a, "out/def", v.Syntax(
cue.Docs(true),
cue.Attributes(true),
cue.Optional(true),
cue.Definitions(true)))
if v.Validate(cue.Concrete(true)) == nil {
e.addFile(e.a, "out/export", v.Syntax(
cue.Concrete(true),
cue.Final(),
cue.Docs(true),
cue.Attributes(true)))
s, err := yaml.Marshal(v)
if err != nil {
fmt.Fprintln(e.header, "#bug: true")
e.warnf("Could not encode as YAML: %v", err)
}
e.a.Files = append(e.a.Files,
txtar.File{Name: "out/yaml", Data: []byte(s)})
b, err := json.Marshal(v)
if err != nil {
fmt.Fprintln(e.header, "#bug: true")
e.warnf("Could not encode as JSON: %v", err)
}
e.a.Files = append(e.a.Files,
txtar.File{Name: "out/json", Data: b})
}
}
func (e *extractor) addFile(a *txtar.Archive, name string, n cueast.Node) {
b, err := cueformat.Node(internal.ToFile(n))
if err != nil {
e.fatalf("Failed to format %s: %v\n", name, err)
}
a.Files = append(a.Files, txtar.File{Name: name, Data: b})
}