blob: ddfd37232405862d7f672223a1f2866ec8683176 [file] [log] [blame] [edit]
/*
Copyright 2016 Google Inc. All Rights Reserved.
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 build
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/bazelbuild/buildtools/tables"
)
// exists reports whether the named file exists.
func exists(name string) bool {
_, err := os.Stat(name)
return err == nil
}
// setFlags sets flags based on the test file name (except the --type flag)
func setFlags(file string) func() {
if strings.Contains(file, ".stripslashes.") {
tables.StripLabelLeadingSlashes = true
}
// Test file 050 tests the ShortenAbsoluteLabelsToRelative behavior, all other tests assume that ShortenAbsoluteLabelsToRelative is false.
if strings.Contains(file, "/050.") {
tables.ShortenAbsoluteLabelsToRelative = true
}
return func() {
tables.StripLabelLeadingSlashes = false
tables.ShortenAbsoluteLabelsToRelative = false
}
}
func testIdempotence(t *testing.T, file string) {
defer setFlags(file)()
testPrint(t, file, file)
}
func testFormat(t *testing.T, input, output string) {
defer setFlags(output)()
testPrint(t, input, output)
}
// Test that reading and then writing the golden files
// does not change their output.
func TestPrintGolden(t *testing.T) {
outs, chdir := findTests(t, ".golden")
defer chdir()
// Run the tests with --type=build
tables.FormattingMode = tables.BuildMode
for _, out := range outs {
if strings.HasSuffix(out, ".bzl.golden") {
continue
}
testIdempotence(t, out)
}
// Run the same tests with --type=bzl
tables.FormattingMode = tables.DefaultMode
for _, out := range outs {
if strings.HasSuffix(out, ".build.golden") {
continue
}
testIdempotence(t, out)
}
tables.FormattingMode = tables.BuildMode
}
// Test that formatting the input files produces the golden files.
func TestPrintRewrite(t *testing.T) {
ins, chdir := findTests(t, ".in")
defer chdir()
for _, in := range ins {
prefix := in[:len(in)-len(".in")]
outBzl := prefix + ".golden"
outBuild := prefix + ".golden"
if !exists(outBzl) {
outBzl = prefix + ".bzl.golden"
outBuild = prefix + ".build.golden"
}
tables.FormattingMode = tables.DefaultMode
testFormat(t, in, outBzl)
tables.FormattingMode = tables.BuildMode
testFormat(t, in, outBuild)
stripslashesBuild := prefix + ".stripslashes.golden"
if exists(stripslashesBuild) {
// Test this file in BUILD mode only
testFormat(t, in, stripslashesBuild)
}
}
}
// findTests finds all files of the passed suffix in the build/testdata directory.
// It changes the working directory to be the directory containing the `testdata` directory,
// and returns a function to call to change back to the current directory.
// This allows tests to assert on alias finding between absolute and relative labels.
func findTests(t *testing.T, suffix string) ([]string, func()) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(filepath.Join(os.Getenv("TEST_SRCDIR"), os.Getenv("TEST_WORKSPACE"), "build")); err != nil {
t.Fatal(err)
}
outs, err := filepath.Glob("testdata/*" + suffix)
if err != nil {
t.Fatal(err)
}
if len(outs) == 0 {
t.Fatal("Didn't find any test cases")
}
return outs, func() { os.Chdir(wd) }
}
// testPrint is a helper for testing the printer.
// It reads the file named in, reformats it, and compares
// the result to the file named out. If rewrite is true, the
// reformatting includes buildifier's higher-level rewrites.
func testPrint(t *testing.T, in, out string) {
data, err := ioutil.ReadFile(in)
if err != nil {
t.Error(err)
return
}
golden, err := ioutil.ReadFile(out)
if err != nil {
t.Error(err)
return
}
base := "testdata/" + filepath.Base(in)
bld, err := Parse(base, data)
if err != nil {
t.Error(err)
return
}
if tables.FormattingMode == tables.BuildMode {
Rewrite(bld, nil)
}
ndata := Format(bld)
if !bytes.Equal(ndata, golden) {
t.Errorf("formatted %s incorrectly: diff shows -%s, +ours", base, filepath.Base(out))
tdiff(t, string(golden), string(ndata))
return
}
}
// Test that when files in the testdata directory are parsed
// and printed and parsed again, we get the same parse tree
// both times.
func TestPrintParse(t *testing.T) {
outs, chdir := findTests(t, "")
defer chdir()
for _, out := range outs {
data, err := ioutil.ReadFile(out)
if err != nil {
t.Error(err)
continue
}
base := "testdata/" + filepath.Base(out)
f, err := Parse(base, data)
if err != nil {
t.Errorf("parsing original: %v", err)
}
ndata := Format(f)
f2, err := Parse(base, ndata)
if err != nil {
t.Errorf("parsing reformatted: %v", err)
}
eq := eqchecker{file: base}
if err := eq.check(f, f2); err != nil {
t.Errorf("not equal: %v", err)
}
}
}
// An eqchecker holds state for checking the equality of two parse trees.
type eqchecker struct {
file string
pos Position
}
// errorf returns an error described by the printf-style format and arguments,
// inserting the current file position before the error text.
func (eq *eqchecker) errorf(format string, args ...interface{}) error {
return fmt.Errorf("%s:%d: %s", eq.file, eq.pos.Line,
fmt.Sprintf(format, args...))
}
// check checks that v and w represent the same parse tree.
// If not, it returns an error describing the first difference.
func (eq *eqchecker) check(v, w interface{}) error {
return eq.checkValue(reflect.ValueOf(v), reflect.ValueOf(w))
}
var (
posType = reflect.TypeOf(Position{})
commentsType = reflect.TypeOf(Comments{})
parenType = reflect.TypeOf((*ParenExpr)(nil))
stringExprType = reflect.TypeOf(StringExpr{})
)
// checkValue checks that v and w represent the same parse tree.
// If not, it returns an error describing the first difference.
func (eq *eqchecker) checkValue(v, w reflect.Value) error {
// inner returns the innermost expression for v.
// If v is a parenthesized expression (X) it returns x.
// if v is a non-nil interface value, it returns the concrete
// value in the interface.
inner := func(v reflect.Value) reflect.Value {
for v.IsValid() {
if v.Type() == parenType {
v = v.Elem().FieldByName("X")
continue
}
if v.Kind() == reflect.Interface && !v.IsNil() {
v = v.Elem()
continue
}
break
}
return v
}
v = inner(v)
w = inner(w)
if v.Kind() != w.Kind() {
return eq.errorf("%s became %s", v.Kind(), w.Kind())
}
// There is nothing to compare for zero values, so exit early.
if !v.IsValid() {
return nil
}
if v.Type() != w.Type() {
return eq.errorf("%s became %s", v.Type(), w.Type())
}
if p, ok := v.Interface().(Expr); ok {
eq.pos, _ = p.Span()
}
switch v.Kind() {
default:
return eq.errorf("unexpected type %s", v.Type())
case reflect.Bool, reflect.Int, reflect.String:
vi := v.Interface()
wi := w.Interface()
if vi != wi {
return eq.errorf("%v became %v", vi, wi)
}
case reflect.Slice:
vl := v.Len()
wl := w.Len()
for i := 0; i < vl || i < wl; i++ {
if i >= vl {
return eq.errorf("unexpected %s", w.Index(i).Type())
}
if i >= wl {
return eq.errorf("missing %s", v.Index(i).Type())
}
if err := eq.checkValue(v.Index(i), w.Index(i)); err != nil {
return err
}
}
case reflect.Struct:
// Fields in struct must match.
t := v.Type()
n := t.NumField()
for i := 0; i < n; i++ {
tf := t.Field(i)
switch {
default:
if err := eq.checkValue(v.Field(i), w.Field(i)); err != nil {
return err
}
case tf.Type == posType: // ignore positions
case tf.Type == commentsType: // ignore comment assignment
case tf.Name == "MultiLine": // ignore multiline setting
case tf.Name == "LineBreak": // ignore line break setting
case t == stringExprType && tf.Name == "Token": // ignore raw string token
}
}
case reflect.Ptr, reflect.Interface:
if v.IsNil() != w.IsNil() {
if v.IsNil() {
return eq.errorf("unexpected %s", w.Elem().Type())
}
return eq.errorf("missing %s", v.Elem().Type())
}
if err := eq.checkValue(v.Elem(), w.Elem()); err != nil {
return err
}
}
return nil
}