blob: 7c22d2b28b343da220d89476aa6f097ab08c2e27 [file] [log] [blame] [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 edit
import (
"fmt"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"testing"
"github.com/bazelbuild/buildtools/build"
)
var parseLabelTests = []struct {
in string
repo string
pkg string
rule string
}{
{"//devtools/buildozer:rule", "", "devtools/buildozer", "rule"},
{"devtools/buildozer:rule", "", "devtools/buildozer", "rule"},
{"//devtools/buildozer", "", "devtools/buildozer", "buildozer"},
{"//base", "", "base", "base"},
{"//base:", "", "base", "base"},
{"@r//devtools/buildozer:rule", "r", "devtools/buildozer", "rule"},
{"@r//devtools/buildozer", "r", "devtools/buildozer", "buildozer"},
{"@r//base", "r", "base", "base"},
{"@r//base:", "r", "base", "base"},
{"@foo", "foo", "", "foo"},
{":label", "", "", "label"},
{"label", "", "", "label"},
{"/abs/path/to/WORKSPACE:rule", "", "/abs/path/to/WORKSPACE", "rule"},
}
func TestParseLabel(t *testing.T) {
for i, tt := range parseLabelTests {
repo, pkg, rule := ParseLabel(tt.in)
if repo != tt.repo || pkg != tt.pkg || rule != tt.rule {
t.Errorf("%d. ParseLabel(%q) => (%q, %q, %q), want (%q, %q, %q)",
i, tt.in, repo, pkg, rule, tt.repo, tt.pkg, tt.rule)
}
}
}
var shortenLabelTests = []struct {
in string
pkg string
result string
}{
{"//devtools/buildozer:rule", "devtools/buildozer", ":rule"},
{"@//devtools/buildozer:rule", "devtools/buildozer", ":rule"},
{"//devtools/buildozer:rule", "devtools", "//devtools/buildozer:rule"},
{"//base:rule", "devtools", "//base:rule"},
{"//base:base", "devtools", "//base"},
{"//base", "base", ":base"},
{"//devtools/buildozer:buildozer", "", "//devtools/buildozer"},
{"@r//devtools/buildozer:buildozer", "devtools/buildozer", "@r//devtools/buildozer"},
{"@r//devtools/buildozer", "devtools/buildozer", "@r//devtools/buildozer"},
{"@r//devtools", "devtools", "@r//devtools"},
{"@r:rule", "", "@r:rule"},
{"@r", "", "@r"},
{"@foo//:foo", "", "@foo"},
{"@foo//devtools:foo", "", "@foo//devtools:foo"},
{"@foo//devtools:foo", "devtools", "@foo//devtools:foo"},
{"@foo//foo:foo", "", "@foo//foo"},
{":local", "", ":local"},
{"something else", "", "something else"},
{"/path/to/file", "path/to", "/path/to/file"},
}
func TestShortenLabel(t *testing.T) {
for i, tt := range shortenLabelTests {
result := ShortenLabel(tt.in, tt.pkg)
if result != tt.result {
t.Errorf("%d. ShortenLabel(%q, %q) => %q, want %q",
i, tt.in, tt.pkg, result, tt.result)
}
}
}
var labelsEqualTests = []struct {
label1 string
label2 string
pkg string
expected bool
}{
{"//devtools/buildozer:rule", "rule", "devtools/buildozer", true},
{"//devtools/buildozer:rule", "rule:jar", "devtools", false},
}
func TestLabelsEqual(t *testing.T) {
for i, tt := range labelsEqualTests {
if got := LabelsEqual(tt.label1, tt.label2, tt.pkg); got != tt.expected {
t.Errorf("%d. LabelsEqual(%q, %q, %q) => %v, want %v",
i, tt.label1, tt.label2, tt.pkg, got, tt.expected)
}
}
}
var splitOnSpacesTests = []struct {
in string
out []string
}{
{"a", []string{"a"}},
{" abc def ", []string{"abc", "def"}},
{` abc\ def `, []string{"abc def"}},
{" abc def\nghi", []string{"abc", "def", "ghi"}},
}
func TestSplitOnSpaces(t *testing.T) {
for i, tt := range splitOnSpacesTests {
result := SplitOnSpaces(tt.in)
if !reflect.DeepEqual(result, tt.out) {
t.Errorf("%d. SplitOnSpaces(%q) => %q, want %q",
i, tt.in, result, tt.out)
}
}
}
func TestInsertLoad(t *testing.T) {
tests := []struct{ input, expected string }{
{``, `load("location", "symbol")`},
{`load("location", "symbol")`, `load("location", "symbol")`},
{`load("location", "other", "symbol")`, `load("location", "other", "symbol")`},
{`load("location", "other")`, `load("location", "other", "symbol")`},
{
`load("other loc", "symbol")`,
`load("location", "symbol")
load("other loc", "symbol")`,
},
}
for _, tst := range tests {
bld, err := build.Parse("BUILD", []byte(tst.input))
if err != nil {
t.Error(err)
continue
}
bld.Stmt = InsertLoad(bld.Stmt, "location", []string{"symbol"}, []string{"symbol"})
got := strings.TrimSpace(string(build.Format(bld)))
if got != tst.expected {
t.Errorf("maybeInsertLoad(%s): got %s, expected %s", tst.input, got, tst.expected)
}
}
}
func TestReplaceLoad(t *testing.T) {
tests := []struct{ input, expected string }{
{
``,
`load("new_location", "symbol")`,
},
{
`load("location", "symbol")`,
`load("new_location", "symbol")`,
},
{
`load("location", "other", "symbol")`,
`load("location", "other")
load("new_location", "symbol")`,
},
{
`load("location", symbol = "other")`,
`load("new_location", "symbol")`,
},
{
`load("other loc", "symbol")
load("location", "symbol")`,
`load("new_location", "symbol")`,
},
}
for _, tst := range tests {
bld, err := build.Parse("BUILD", []byte(tst.input))
if err != nil {
t.Error(err)
continue
}
bld.Stmt = ReplaceLoad(bld.Stmt, "new_location", []string{"symbol"}, []string{"symbol"})
got := strings.TrimSpace(string(build.Format(bld)))
if got != tst.expected {
t.Errorf("maybeReplaceLoad(%s): got %s, expected %s", tst.input, got, tst.expected)
}
}
}
func TestAddValueToListAttribute(t *testing.T) {
tests := []struct{ input, expected string }{
{`rule(name="rule")`, `rule(name="rule", attr=["foo"])`},
{`rule(name="rule", attr=["foo"])`, `rule(name="rule", attr=["foo"])`},
{`rule(name="rule", attr=IDENT)`, `rule(name="rule", attr=IDENT+["foo"])`},
{`rule(name="rule", attr=["foo"] + IDENT)`, `rule(name="rule", attr=["foo"] + IDENT)`},
{`rule(name="rule", attr=["bar"] + IDENT)`, `rule(name="rule", attr=["bar", "foo"] + IDENT)`},
{`rule(name="rule", attr=IDENT + ["foo"])`, `rule(name="rule", attr=IDENT + ["foo"])`},
{`rule(name="rule", attr=IDENT + ["bar"])`, `rule(name="rule", attr=IDENT + ["bar", "foo"])`},
}
for _, tst := range tests {
bld, err := build.Parse("BUILD", []byte(tst.input))
if err != nil {
t.Error(err)
continue
}
rule := bld.RuleAt(1)
AddValueToListAttribute(rule, "attr", "", &build.StringExpr{Value: "foo"}, nil)
got := strings.TrimSpace(string(build.Format(bld)))
wantBld, err := build.Parse("BUILD", []byte(tst.expected))
if err != nil {
t.Error(err)
continue
}
want := strings.TrimSpace(string(build.Format(wantBld)))
if got != want {
t.Errorf("AddValueToListAttribute(%s): got %s, expected %s", tst.input, got, want)
}
}
}
func TestSelectListsIntersection(t *testing.T) {
tests := []struct {
input string
expected []build.Expr
}{
{`rule(
name = "rule",
attr = select()
)`, nil},
{`rule(
name = "rule",
attr = select({})
)`, nil},
{`rule(
name = "rule",
attr = select(CONFIGS)
)`, nil},
{`rule(
name = "rule",
attr = select({
"config": "string",
"DEFAULT": "default"
})
)`, nil},
{`rule(
name = "rule",
attr = select({
"config": LIST,
"DEFAULT": DEFAULT
})
)`, nil},
{`rule(
name = "rule",
attr = select({
"config": ":1 :2 :3".split(" "),
"DEFAULT": ":2 :3".split(" ")
})
)`, nil},
{`rule(
name = "rule",
attr = select({
"config1": [":1"],
"config2": [":2"],
"DEFAULT": []
})
)`, []build.Expr{}},
{`rule(
name = "rule",
attr = select({
"config1": [],
"config2": [":1"],
"DEFAULT": [":1"]
})
)`, []build.Expr{}},
{`rule(
name = "rule",
attr = select({
"config1": [":1", ":2", ":3"],
"config2": [":2"],
"config3": [":2", ":3"],
"DEFAULT": [":1", ":2"]
})
)`, []build.Expr{&build.StringExpr{Value: ":2"}}},
{`rule(
name = "rule",
attr = select({
"config1": [":4", ":3", ":1", ":5", ":2", ":6"],
"config2": [":5", ":2", ":6", ":1"],
"config3": [":1", ":2", ":3", ":4", ":5", ":6"],
"config4": [":2", ":1"],
"DEFAULT": [":3", ":4", ":1", ":2"]
})
)`, []build.Expr{&build.StringExpr{Value: ":1"}, &build.StringExpr{Value: ":2"}}},
}
for _, tst := range tests {
bld, err := build.Parse("BUILD", []byte(tst.input))
if err != nil {
t.Error(err)
continue
}
rule := bld.RuleAt(1)
got := SelectListsIntersection(rule.Attr("attr").(*build.CallExpr), "")
errStr := fmt.Sprintf("TestSelectListsIntersection(%s): got %s, expected %s", tst.input, got, tst.expected)
if len(got) != len(tst.expected) {
t.Error(errStr)
}
for i := range got {
if got[i].(*build.StringExpr).Value != tst.expected[i].(*build.StringExpr).Value {
t.Error(errStr)
}
}
}
}
func TestRemoveEmptySelectsAndConcatLists(t *testing.T) {
tests := []struct{ input, expected string }{
{`rule(
name = "rule",
attr = select({
"config1": [],
"config2": [],
"DEFAULT": []
})
)`, `rule(
name = "rule",
attr = []
)`},
{`rule(
name = "rule",
attr = select({}) + select() + select({
"config1": [],
"config2": [],
"DEFAULT": []
})
)`, `rule(
name = "rule",
attr = []
)`},
{`rule(
name = "rule",
attr = select({
"config1": [],
"config2": [],
"DEFAULT": []
}) + select(CONFIGS)
)`, `rule(
name = "rule",
attr = select(CONFIGS)
)`},
{`rule(
name = "rule",
attr = [":1"] + select({
"config1": [],
"config2": [],
"DEFAULT": []
}) + [":2"]
)`, `rule(
name = "rule",
attr = [":1", ":2"]
)`},
{`rule(
name = "rule",
attr = [":1"] + select({
"config1": [],
"config2": [],
"DEFAULT": []
}) + LIST + [":2"]
)`, `rule(
name = "rule",
attr = [":1"] + LIST + [":2"]
)`},
{`rule(
name = "rule",
attr = [":1"] + [":2", ":3"] + select({
"config1": [":4"],
"config2": [],
"DEFAULT": []
}) + []
)`, `rule(
name = "rule",
attr = [":1", ":2", ":3"] + select({
"config1": [":4"],
"config2": [],
"DEFAULT": []
})
)`},
{`rule(
name = "rule",
attr = [":1"] + [":2", ":3"] + select({
"config1": [":4"],
"config2": [],
"DEFAULT": []
}) + [] + select({
"config": LIST,
"DEFAULT": DEFAULT,
})
)`, `rule(
name = "rule",
attr = [":1", ":2", ":3"] + select({
"config1": [":4"],
"config2": [],
"DEFAULT": []
}) + select({
"config": LIST,
"DEFAULT": DEFAULT,
})
)`},
}
for _, tst := range tests {
bld, err := build.Parse("BUILD", []byte(tst.input))
if err != nil {
t.Error(err)
continue
}
rule := bld.RuleAt(1)
rule.SetAttr("attr", RemoveEmptySelectsAndConcatLists(rule.Attr("attr")))
got := strings.TrimSpace(string(build.Format(bld)))
wantBld, err := build.Parse("BUILD", []byte(tst.expected))
if err != nil {
t.Error(err)
continue
}
want := strings.TrimSpace(string(build.Format(wantBld)))
if got != want {
t.Errorf("RemoveEmptySelectsAndConcatLists(%s):\n got: %s,\n expected: %s", tst.input, got, want)
}
}
}
func TestResolveAttr(t *testing.T) {
tests := []struct{ input, expected string }{
{`rule(
name = "rule",
attr = select({
"config1": [":1"],
"config2": [":1"],
"DEFAULT": [":1"]
})
)`, `rule(
name = "rule",
attr = [":1"]
)`},
{`rule(
name = "rule",
attr = select({
"config1": [":1"],
"config2": [":1"],
"DEFAULT": [":1"]
}) + select() + select({})
)`, `rule(
name = "rule",
attr = [":1"]
)`},
{`rule(
name = "rule",
attr = select({
"config1": [":1"],
"config2": [":1"],
"DEFAULT": [":1"]
}) + LIST
)`, `rule(
name = "rule",
attr = LIST + [":1"]
)`},
{`rule(
name = "rule",
attr = select({
"config1": [":1"],
"config2": [":1"],
"DEFAULT": [":1"]
}) + select({
"config": LIST,
"DEFAULT": DEFAULT
}) + select({
"config": ":2 :3".split(" "),
"DEFAULT": ":3".split(" ")
})
)`, `rule(
name = "rule",
attr = select({
"config": LIST,
"DEFAULT": DEFAULT
}) + select({
"config": ":2 :3".split(" "),
"DEFAULT": ":3".split(" ")
}) + [":1"]
)`},
{`rule(
name = "rule",
attr = [":1"] + select({
"config1": [":2"],
"config2": [":2"],
"DEFAULT": [":2"]
}) + [":3"] + select({
"config1": [":4", ":2"],
"DEFAULT": [":2"]
})
)`, `rule(
name = "rule",
attr = [":1", ":2", ":3"] + select({
"config1": [":4"],
"DEFAULT": []
})
)`},
{`rule(
name = "rule",
attr = [":1"] + select({
"config1": [":2"],
"config2": [":2"],
"DEFAULT": [":2"]
}) + [":3"] + select({
"config1": [":4", ":2"],
"DEFAULT": [":4", ":2"]
})
)`, `rule(
name = "rule",
attr = [":1", ":2", ":4", ":3"]
)`},
}
for _, tst := range tests {
bld, err := build.Parse("BUILD", []byte(tst.input))
if err != nil {
t.Error(err)
continue
}
rule := bld.RuleAt(1)
ResolveAttr(rule, "attr", "")
got := strings.TrimSpace(string(build.Format(bld)))
wantBld, err := build.Parse("BUILD", []byte(tst.expected))
if err != nil {
t.Error(err)
continue
}
want := strings.TrimSpace(string(build.Format(wantBld)))
if got != want {
t.Errorf("ResolveAttr(%s):\n got: %s\n expected: %s", tst.input, got, want)
}
}
}
func TestListSubstitute(t *testing.T) {
tests := []struct {
desc, input, oldPattern, newTemplate, want string
}{
{
desc: "no_match",
input: `["abc"]`,
oldPattern: `!!`,
newTemplate: `xx`,
want: `["abc"]`,
}, {
desc: "full_match",
input: `["abc"]`,
oldPattern: `.*`,
newTemplate: `xx`,
want: `["xx"]`,
}, {
desc: "partial_match",
input: `["abcde"]`,
oldPattern: `bcd`,
newTemplate: `xyz`,
want: `["axyze"]`,
}, {
desc: "number_group",
input: `["abcde"]`,
oldPattern: `a(bcd)`,
newTemplate: `$1 $1`,
want: `["bcd bcde"]`,
}, {
desc: "name_group",
input: `["abcde"]`,
oldPattern: `a(?P<x>bcd)`,
newTemplate: `$x $x`,
want: `["bcd bcde"]`,
},
}
for _, tst := range tests {
t.Run(tst.desc, func(t *testing.T) {
f, err := build.ParseBuild("BUILD", []byte(tst.input))
if err != nil {
t.Fatalf("parse error: %v", err)
}
lst := f.Stmt[0]
oldRegexp, err := regexp.Compile(tst.oldPattern)
if err != nil {
t.Fatalf("error compiling regexp %q: %v", tst.oldPattern, err)
}
ListSubstitute(lst, oldRegexp, tst.newTemplate)
if got := build.FormatString(lst); got != tst.want {
t.Errorf("ListSubstitute(%q, %q, %q) = %q ; want %q", tst.input, tst.oldPattern, tst.newTemplate, got, tst.want)
}
})
}
}
func compareKeyValue(a, b build.Expr) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
aKeyVal := a.(*build.KeyValueExpr)
bKeyVal := b.(*build.KeyValueExpr)
return aKeyVal.Key.(*build.StringExpr).Value == bKeyVal.Key.(*build.StringExpr).Value &&
aKeyVal.Value.(*build.StringExpr).Value == bKeyVal.Value.(*build.StringExpr).Value
}
func TestDictionaryDelete(t *testing.T) {
tests := []struct {
input, expected string
expectedReturn build.Expr
}{
{
`rule(attr = {"deletekey": "value"})`,
`rule(attr = {})`,
&build.KeyValueExpr{
Key: &build.StringExpr{Value: "deletekey"},
Value: &build.StringExpr{Value: "value"},
},
}, {
`rule(attr = {"nodeletekey": "value", "deletekey": "value"})`,
`rule(attr = {"nodeletekey": "value"})`,
&build.KeyValueExpr{
Key: &build.StringExpr{Value: "deletekey"},
Value: &build.StringExpr{Value: "value"},
},
}, {
`rule(attr = {"nodeletekey": "value"})`,
`rule(attr = {"nodeletekey": "value"})`,
nil,
},
}
for _, tst := range tests {
bld, err := build.ParseBuild("BUILD", []byte(tst.input))
if err != nil {
t.Error(err)
continue
}
rule := bld.RuleAt(1)
dict := rule.Call.List[0].(*build.AssignExpr).RHS.(*build.DictExpr)
returnVal := DictionaryDelete(dict, "deletekey")
got := strings.TrimSpace(string(build.Format(bld)))
wantBld, err := build.Parse("BUILD", []byte(tst.expected))
if err != nil {
t.Error(err)
continue
}
want := strings.TrimSpace(string(build.Format(wantBld)))
if got != want {
t.Errorf("TestDictionaryDelete(%s): got %s, expected %s", tst.input, got, want)
}
if !compareKeyValue(returnVal, tst.expectedReturn) {
t.Errorf("TestDictionaryDelete(%s): returned %v, expected %v", tst.input, returnVal, tst.expectedReturn)
}
}
}
func TestPackageDeclaration(t *testing.T) {
tests := []struct{ input, expected string }{
{``, `package(attr = "val")`},
{`"""Docstring."""
load(":path.bzl", "x")
# package() comes here
x = 2`,
`"""Docstring."""
load(":path.bzl", "x")
# package() comes here
package(attr = "val")
x = 2`,
},
}
for _, tst := range tests {
bld, err := build.Parse("BUILD", []byte(tst.input))
if err != nil {
t.Error(err)
continue
}
pkg := PackageDeclaration(bld)
pkg.SetAttr("attr", &build.StringExpr{Value: "val"})
got := strings.TrimSpace(string(build.Format(bld)))
want := strings.TrimSpace(tst.expected)
if got != want {
t.Errorf("TestPackageDeclaration: got:\n%s\nexpected:\n%s", got, want)
}
}
}
type testCase struct {
inputRoot, inputTarget string
expectedBuildFile, expectedPkg, expectedRule string
}
func runTestInterpretLabelForWorkspaceLocation(t *testing.T, buildFileName string) {
tmp, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
if err := os.MkdirAll(filepath.Join(tmp, "a", "b"), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tmp, "WORKSPACE"), nil, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tmp, buildFileName), nil, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tmp, "a", buildFileName), nil, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tmp, "a", "b", buildFileName), nil, 0755); err != nil {
t.Fatal(err)
}
for _, tc := range []testCase{
{tmp, "//", filepath.Join(tmp, buildFileName), "", "."},
{tmp, "//a", filepath.Join(tmp, "a", buildFileName), "a", "a"},
{tmp, "//a:a", filepath.Join(tmp, "a", buildFileName), "a", "a"},
{tmp, "//a/b", filepath.Join(tmp, "a", "b", buildFileName), "a/b", "b"},
{tmp, "//a/b:b", filepath.Join(tmp, "a", "b", buildFileName), "a/b", "b"},
} {
buildFile, _, pkg, rule := InterpretLabelForWorkspaceLocation(tc.inputRoot, tc.inputTarget)
if buildFile != tc.expectedBuildFile || pkg != tc.expectedPkg || rule != tc.expectedRule {
t.Errorf("InterpretLabelForWorkspaceLocation(%q, %q) = %q, %q, %q; want %q, %q, %q", tc.inputRoot, tc.inputTarget, buildFile, pkg, rule, tc.expectedBuildFile, tc.expectedPkg, tc.expectedRule)
}
}
}
func TestInterpretLabelForWorkspaceLocation(t *testing.T) {
runTestInterpretLabelForWorkspaceLocation(t, "BUILD")
runTestInterpretLabelForWorkspaceLocation(t, "BUILD.bazel")
}