blob: b9f7b5f035d74fded2c76ce1f7d19a7bd5de2833 [file] [edit]
/*
Copyright 2016 Google LLC
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
https://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 (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/bazelbuild/buildtools/testutils"
)
func TestParse(t *testing.T) {
for i, tt := range parseTests {
name := "test"
if tt.out != nil {
name = tt.out.Path
}
p, err := Parse(name, []byte(tt.in))
if err != nil {
t.Errorf("#%d: %v", i, err)
continue
}
if tt.out != nil {
compare(t, p, tt.out)
}
}
}
func TestParseTestdata(t *testing.T) {
// Test that files in the testdata directory can all be parsed.
// For this test we don't bother checking what the tree looks like.
// The printing tests will exercise that information.
testdata := os.Getenv("TEST_SRCDIR") + "/" + os.Getenv("TEST_WORKSPACE") + "/build/testdata"
outs, err := filepath.Glob(testdata + "/*")
if err != nil {
t.Fatal(err)
}
if len(outs) == 0 {
t.Fatal("Data set is empty:", testdata)
}
for _, out := range outs {
if strings.HasSuffix(out, ".error") {
// Incorrect starlark file, skip
continue
}
data, err := os.ReadFile(out)
if err != nil {
t.Error(err)
continue
}
_, err = Parse(filepath.Base(out), data)
if err != nil {
t.Error(err)
}
}
}
// toJSON returns human-readable json for the given syntax tree.
// It is used as input to diff for comparing the actual syntax tree with the expected one.
func toJSON(v interface{}) string {
s, _ := json.MarshalIndent(v, "", "\t")
s = append(s, '\n')
return string(s)
}
// Compare expected and actual values, failing and outputting a diff of the two values if they are not deeply equal
func compare(t *testing.T, actual, expected interface{}) {
if !reflect.DeepEqual(expected, actual) {
testutils.Tdiff(t, []byte(toJSON(expected)), []byte(toJSON(actual)))
}
}
// Small tests checking that the parser returns exactly the right syntax tree.
// If out is nil, we only check that the parser accepts the file.
var parseTests = []struct {
in string
out *File
}{
{
in: `go_binary(name = "x"
)
`,
out: &File{
Path: "BUILD",
Type: TypeBuild,
Stmt: []Expr{
&CallExpr{
X: &Ident{
NamePos: Position{1, 1, 0},
Name: "go_binary",
},
ListStart: Position{1, 10, 9},
List: []Expr{
&AssignExpr{
LHS: &Ident{
NamePos: Position{1, 11, 10},
Name: "name",
},
OpPos: Position{1, 16, 15},
Op: "=",
RHS: &StringExpr{
Start: Position{1, 18, 17},
Value: "x",
End: Position{1, 21, 20},
Token: `"x"`,
},
},
},
End: End{Pos: Position{2, 1, 21}},
ForceMultiLine: true,
},
},
},
},
{
in: `foo.bar.baz(name = "x")`,
out: &File{
Path: "test",
Type: TypeDefault,
Stmt: []Expr{
&CallExpr{
X: &DotExpr{
X: &DotExpr{
X: &Ident{
NamePos: Position{1, 1, 0},
Name: "foo",
},
Dot: Position{1, 4, 3},
NamePos: Position{1, 5, 4},
Name: "bar",
},
Dot: Position{1, 8, 7},
NamePos: Position{1, 9, 8},
Name: "baz",
},
ListStart: Position{1, 12, 11},
List: []Expr{
&AssignExpr{
LHS: &Ident{
NamePos: Position{1, 13, 12},
Name: "name",
},
OpPos: Position{1, 18, 17},
Op: "=",
RHS: &StringExpr{
Start: Position{1, 20, 19},
Value: "x",
End: Position{1, 23, 22},
Token: `"x"`,
},
},
},
End: End{Pos: Position{1, 23, 22}},
},
},
},
},
{
in: `package(default_visibility = ["//visibility:legacy_public"])
`,
},
{
in: `__unused__ = [ foo_binary(
name = "signed_release_%sdpi" % dpi,
srcs = [
":aps_release_%s" % dpi, # all of Maps, obfuscated, w/o NLP
":qlp_release_%s" % dpi, # the NLP
":check_binmode_release",
":check_remote_strings_release",
],
debug_key = "//foo:bar.baz",
resources = ":R_src_release_%sdpi" % dpi)
for dpi in dpis ]
`,
},
{
in: `load(":foo.bzl", "foo", """bar""", baz="foo", foo="""baz""")
`,
out: &File{
Path: "BUILD",
Type: TypeBuild,
Stmt: []Expr{
&LoadStmt{
Load: Position{1, 1, 0},
Module: &StringExpr{
Value: ":foo.bzl",
Token: "\":foo.bzl\"",
Start: Position{1, 6, 5},
End: Position{1, 16, 15},
},
From: []*Ident{
{
Name: "foo",
NamePos: Position{1, 19, 18},
},
{
Name: "bar",
NamePos: Position{1, 28, 27},
},
{
Name: "foo",
NamePos: Position{1, 41, 40},
},
{
Name: "baz",
NamePos: Position{1, 54, 53},
},
},
To: []*Ident{
{
Name: "foo",
NamePos: Position{1, 19, 18},
},
{
Name: "bar",
NamePos: Position{1, 28, 27},
},
{
Name: "baz",
NamePos: Position{1, 36, 35},
},
{
Name: "foo",
NamePos: Position{1, 47, 46},
},
},
Rparen: End{Pos: Position{1, 60, 59}},
ForceCompact: true,
},
},
},
},
{
in: `type OptionalDict[T, U] = dict[T, U] | None`,
out: &File{
Path: "test",
Type: TypeDefault,
Stmt: []Expr{
&TypeAliasStmt{
TypePos: Position{Line: 1, LineRune: 1, Byte: 0},
Name: &Ident{
Name: "OptionalDict",
NamePos: Position{Line: 1, LineRune: 6, Byte: 5},
},
TypeParams: &ListExpr{
Start: Position{Line: 1, LineRune: 18, Byte: 17},
List: []Expr{
&Ident{
Name: "T",
NamePos: Position{Line: 1, LineRune: 19, Byte: 18},
},
&Ident{
Name: "U",
NamePos: Position{Line: 1, LineRune: 22, Byte: 21},
},
},
End: End{Pos: Position{Line: 1, LineRune: 23, Byte: 22}},
},
EqualPos: Position{Line: 1, LineRune: 25, Byte: 24},
Type: &BinaryExpr{
X: &TypeAppExpr{
Type: &Ident{
Name: "dict",
NamePos: Position{Line: 1, LineRune: 27, Byte: 26},
},
ArgsStart: Position{Line: 1, LineRune: 31, Byte: 30},
Args: []Expr{
&Ident{
Name: "T",
NamePos: Position{Line: 1, LineRune: 32, Byte: 31},
},
&Ident{
Name: "U",
NamePos: Position{Line: 1, LineRune: 35, Byte: 34},
},
},
End: End{Pos: Position{Line: 1, LineRune: 36, Byte: 35}},
ForceCompact: true,
},
Op: "|",
OpStart: Position{Line: 1, LineRune: 38, Byte: 37},
Y: &Ident{
Name: "None",
NamePos: Position{Line: 1, LineRune: 40, Byte: 39},
},
},
},
},
},
},
{
in: `type T = typing.Sequence[int]`,
out: &File{
Path: "test",
Type: TypeDefault,
Stmt: []Expr{
&TypeAliasStmt{
TypePos: Position{Line: 1, LineRune: 1, Byte: 0},
Name: &Ident{
Name: "T",
NamePos: Position{Line: 1, LineRune: 6, Byte: 5},
},
EqualPos: Position{Line: 1, LineRune: 8, Byte: 7},
Type: &TypeAppExpr{
Type: &DotExpr{
X: &Ident{
Name: "typing",
NamePos: Position{Line: 1, LineRune: 10, Byte: 9},
},
Dot: Position{Line: 1, LineRune: 16, Byte: 15},
NamePos: Position{Line: 1, LineRune: 17, Byte: 16},
Name: "Sequence",
},
ArgsStart: Position{Line: 1, LineRune: 25, Byte: 24},
Args: []Expr{
&Ident{
Name: "int",
NamePos: Position{Line: 1, LineRune: 26, Byte: 25},
},
},
End: End{Pos: Position{Line: 1, LineRune: 29, Byte: 28}},
},
},
},
},
},
}
func TestParseError(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "no trailing commas in type applications",
in: "x: dict[str, int,]",
want: "test:1:19: syntax error near ]",
},
// type alias errors
{
name: "type alias not at top level",
in: "def f():\n type T = int\n",
want: "test:2:3: syntax error: type alias not at top level",
},
{
name: "misspelled type soft keyword",
in: "tpye MyInt = int\n",
want: "test:1:11: syntax error near MyInt",
},
{
// Check that 'type' soft keyword support doesn't break syntax error handling for
// other occurrences of one identifier followed by another.
name: "ident followed by ident syntax error",
in: "foo bar\n",
want: "test:1:8: syntax error near bar",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Parse("test", []byte(tt.in))
if err == nil {
t.Fatalf("Parse(%q) succeeded, want error %q", tt.in, tt.want)
}
if got := err.Error(); got != tt.want {
t.Errorf("Parse(%q) error = %q, want %q", tt.in, got, tt.want)
}
})
}
}