blob: c78e12338ecf922b6db431bd74a63f033fba7098 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package genparams
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/dave/dst"
"github.com/dave/dst/decorator"
"github.com/dave/dst/dstutil"
"chromiumos/tast/caller"
)
// envName is the name of the environment variable that instructs Ensure to
// update source files with generated metadata instead of just comparing them.
const envName = "TAST_GENERATE_UPDATE"
// Ensure ensures that parameterized test parameters in a test metadata are
// up-to-date.
//
// file is a file name of a .go file containing a Tast test definition (i.e.
// testing.AddTest call). params is a Go literal expression as a string that
// represents test parameters.
//
// If TAST_GENERATE_UPDATE environment variable is not set, this function checks
// if parameterized test parameters in file matches with params. If any mismatch
// is found, it reports a test error with a message prompting users to
// regenerate parameterized test parameters.
//
// If TAST_GENERATE_UPDATE environment variable is set, this function rewrites
// the testing.Test literal in file with params.
func Ensure(t TestingT, file, params string) {
t.Helper()
oldCode, err := ioutil.ReadFile(file)
if err != nil {
t.Fatalf("%s: %v", file, err)
}
// Construct DST of the source code.
root, err := decorator.Parse(oldCode)
if err != nil {
t.Fatalf("%s: %v", file, err)
}
// Ensure that chromiumos/tast/testing is imported without alias.
for _, im := range root.Imports {
path, err := strconv.Unquote(im.Path.Value)
if err != nil {
continue
}
if path == "chromiumos/tast/testing" {
if im.Name != nil {
t.Fatalf("chromiumos/tast/testing must be imported without alias")
}
}
}
// Construct DST of the new metadata.
_, testPath, _, _ := runtime.Caller(1)
paramsCode := fmt.Sprintf(`package main
var _ = []testing.Param{
// Parameters generated by %s. DO NOT EDIT.
%s
}`, filepath.Base(testPath), params)
paramsRoot, err := decorator.Parse(paramsCode)
if err != nil {
t.Errorf("%s: %v", file, err)
return
}
paramsExpr := paramsRoot.Decls[0].(*dst.GenDecl).Specs[0].(*dst.ValueSpec).Values[0]
// Replace Params in testing.Test literals.
hits := 0
dstutil.Apply(root, func(cur *dstutil.Cursor) bool {
// Match with testing.Test composite literal.
comp, ok := cur.Node().(*dst.CompositeLit)
if !ok {
return true
}
sel, ok := comp.Type.(*dst.SelectorExpr)
if !ok || sel.Sel.Name != "Test" {
return true
}
if id, ok := sel.X.(*dst.Ident); !ok || id.Name != "testing" {
return true
}
// Delete Params if it exists.
elts := append([]dst.Expr(nil), comp.Elts...)
for i, elt := range elts {
kv, ok := elt.(*dst.KeyValueExpr)
if !ok {
continue
}
if id, ok := kv.Key.(*dst.Ident); !ok || id.Name != "Params" {
continue
}
elts = append(elts[:i], elts[i+1:]...)
break
}
// Append Params to the composite literal.
elts = append(elts, &dst.KeyValueExpr{
Key: &dst.Ident{Name: "Params"},
Value: paramsExpr,
Decs: dst.KeyValueExprDecorations{
NodeDecs: dst.NodeDecs{
After: dst.NewLine,
},
},
})
repl := &dst.CompositeLit{
Type: &dst.SelectorExpr{
X: &dst.Ident{Name: "testing"},
Sel: &dst.Ident{Name: "Test"},
},
Elts: elts,
}
cur.Replace(repl)
hits++
return false
}, nil)
if hits != 1 {
t.Fatalf("%s: found %d testing.AddTest calls; want 1", file, hits)
}
var b bytes.Buffer
if err := decorator.Fprint(&b, root); err != nil {
t.Fatalf("%s: %v", file, err)
}
newCode := b.Bytes()
// If the environment variable is set, update the source code.
if os.Getenv(envName) != "" {
if err := ioutil.WriteFile(file, newCode, 0666); err != nil {
t.Fatalf("%s: failed to save the generated code: %v", file, err)
}
return
}
if !bytes.Equal(newCode, oldCode) {
pkg := strings.Split(caller.Get(2), ".")[0]
t.Errorf(`%s: Params is stale; run the following command to update:
%s=1 ~/trunk/src/platform/tast/tools/go.sh test -count=1 %s`, file, envName, pkg)
}
}