/* Copyright 2018 The Bazel Authors. 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 rule import ( "path/filepath" "reflect" "sort" "strings" "testing" bzl "github.com/bazelbuild/buildtools/build" ) // This file contains tests for File, Load, Rule, and related functions. // Tests only cover some basic functionality and a few non-obvious cases. // Most test coverage will come from clients of this package. func TestEditAndSync(t *testing.T) { old := []byte(` load("a.bzl", "x_library") x_library(name = "foo") load("b.bzl", y_library = "y") y_library(name = "bar") `) f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", old) if err != nil { t.Fatal(err) } loadA := f.Loads[0] loadA.Delete() loadB := f.Loads[1] loadB.Add("x_library") loadB.Remove("y_library") loadC := NewLoad("c.bzl") loadC.AddAlias("z_library", "z") loadC.Add("y_library") loadC.Insert(f, 3) foo := f.Rules[0] foo.Delete() bar := f.Rules[1] bar.SetAttr("srcs", []string{"bar.y"}) loadMaybe := NewLoad("//some:maybe.bzl") loadMaybe.Add("maybe") loadMaybe.Insert(f, 0) baz := NewRule("maybe", "baz") baz.AddArg(&bzl.LiteralExpr{Token: "z"}) baz.SetAttr("srcs", GlobValue{ Patterns: []string{"**"}, Excludes: []string{"*.pem"}, }) baz.Insert(f) got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(` load("//some:maybe.bzl", "maybe") load("b.bzl", "x_library") load( "c.bzl", "y_library", z = "z_library", ) y_library( name = "bar", srcs = ["bar.y"], ) maybe( z, name = "baz", srcs = glob( ["**"], exclude = ["*.pem"], ), ) `) if got != want { t.Errorf("got:\n%s\nwant:\n%s", got, want) } } func TestPassInserted(t *testing.T) { old := []byte(` load("a.bzl", "baz") def foo(): go_repository(name = "bar") `) f, err := LoadMacroData(filepath.Join("old", "repo.bzl"), "", "foo", old) if err != nil { t.Fatal(err) } f.Rules[0].Delete() f.Sync() got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(` load("a.bzl", "baz") def foo(): pass `) if got != want { t.Errorf("got:\n%s\nwant:%s", got, want) } } func TestPassRemoved(t *testing.T) { old := []byte(` load("a.bzl", "baz") def foo(): pass `) f, err := LoadMacroData(filepath.Join("old", "repo.bzl"), "", "foo", old) if err != nil { t.Fatal(err) } bar := NewRule("go_repository", "bar") bar.Insert(f) f.Sync() got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(` load("a.bzl", "baz") def foo(): go_repository(name = "bar") `) if got != want { t.Errorf("got:\n%s\nwant:%s", got, want) } } func TestFunctionInserted(t *testing.T) { f, err := LoadMacroData(filepath.Join("old", "repo.bzl"), "", "foo", nil) if err != nil { t.Fatal(err) } bar := NewRule("go_repository", "bar") bar.Insert(f) f.Sync() got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(` def foo(): go_repository(name = "bar") `) if got != want { t.Errorf("got:\n%s\nwant:%s", got, want) } } func TestArgsAlwaysEndUpBeforeKwargs(t *testing.T) { f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", nil) if err != nil { t.Fatal(err) } bar := NewRule("maybe", "bar") bar.SetAttr("url", "https://doesnotexist.com") bar.AddArg(&bzl.Ident{Name: "http_archive"}) bar.Insert(f) f.Sync() got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(` maybe( http_archive, name = "bar", url = "https://doesnotexist.com", ) `) if got != want { t.Errorf("got:\n%s\nwant:%s", got, want) } } func TestDeleteSyncDelete(t *testing.T) { old := []byte(` x_library(name = "foo") # comment x_library(name = "bar") `) f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", old) if err != nil { t.Fatal(err) } foo := f.Rules[0] bar := f.Rules[1] foo.Delete() f.Sync() bar.Delete() f.Sync() got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(`# comment`) if got != want { t.Errorf("got:\n%s\nwant:%s", got, want) } } func TestInsertDeleteSync(t *testing.T) { old := []byte("") f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", old) if err != nil { t.Fatal(err) } foo := NewRule("filegroup", "test") foo.Insert(f) foo.Delete() f.Sync() got := strings.TrimSpace(string(f.Format())) want := "" if got != want { t.Errorf("got:\n%s\nwant:%s", got, want) } } func TestSymbolsReturnsKeys(t *testing.T) { f, err := LoadData(filepath.Join("load", "BUILD.bazel"), "", []byte(`load("a.bzl", "y", z = "a")`)) if err != nil { t.Fatal(err) } got := f.Loads[0].Symbols() want := []string{"y", "z"} if !reflect.DeepEqual(got, want) { t.Errorf("got %#v; want %#v", got, want) } } func TestLoadCommentsAreRetained(t *testing.T) { f, err := LoadData(filepath.Join("load", "BUILD.bazel"), "", []byte(` load( "a.bzl", # Comment for a symbol that will be deleted. "baz", # Some comment without remapping. "foo", # Some comment with remapping. my_bar = "bar", ) `)) if err != nil { t.Fatal(err) } l := f.Loads[0] l.Remove("baz") f.Sync() l.Add("new_baz") f.Sync() got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(` load( "a.bzl", # Some comment without remapping. "foo", "new_baz", # Some comment with remapping. my_bar = "bar", ) `) if got != want { t.Errorf("got:\n%s\nwant:%s", got, want) } } func TestKeepRule(t *testing.T) { for _, tc := range []struct { desc, src string want bool }{ { desc: "prefix", src: ` # keep x_library(name = "x") `, want: true, }, { desc: "prefix with description", src: ` # keep: hack, see more in ticket #42 x_library(name = "x") `, want: true, }, { desc: "compact_suffix", src: ` x_library(name = "x") # keep `, want: true, }, { desc: "multiline_internal", src: ` x_library( # keep name = "x", ) `, want: false, }, { desc: "multiline_suffix", src: ` x_library( name = "x", ) # keep `, want: true, }, { desc: "after", src: ` x_library(name = "x") # keep `, want: false, }, } { t.Run(tc.desc, func(t *testing.T) { f, err := LoadData(filepath.Join(tc.desc, "BUILD.bazel"), "", []byte(tc.src)) if err != nil { t.Fatal(err) } if got := f.Rules[0].ShouldKeep(); got != tc.want { t.Errorf("got %v; want %v", got, tc.want) } }) } } func TestShouldKeepExpr(t *testing.T) { for _, tc := range []struct { desc, src string path func(e bzl.Expr) bzl.Expr want bool }{ { desc: "before", src: ` # keep "s" `, want: true, }, { desc: "before with description", src: ` # keep: we need it for the ninja feature "s" `, want: true, }, { desc: "before but not the correct prefix (keeping)", src: ` # keeping this for now "s" `, want: false, }, { desc: "before but not the correct prefix (no colon)", src: ` # keep this around for the time being "s" `, want: false, }, { desc: "suffix", src: ` "s" # keep `, want: true, }, { desc: "after", src: ` "s" # keep `, want: false, }, { desc: "list_elem_prefix", src: ` [ # keep "s", ] `, path: func(e bzl.Expr) bzl.Expr { return e.(*bzl.ListExpr).List[0] }, want: true, }, { desc: "list_elem_suffix", src: ` [ "s", # keep ] `, path: func(e bzl.Expr) bzl.Expr { return e.(*bzl.ListExpr).List[0] }, want: true, }, } { t.Run(tc.desc, func(t *testing.T) { ast, err := bzl.Parse(tc.desc, []byte(tc.src)) if err != nil { t.Fatal(err) } expr := ast.Stmt[0] if tc.path != nil { expr = tc.path(expr) } got := ShouldKeep(expr) if got != tc.want { t.Errorf("got %v; want %v", got, tc.want) } }) } } func TestInternalVisibility(t *testing.T) { tests := []struct { rel string expected string }{ {rel: "internal", expected: "//:__subpackages__"}, {rel: "a/b/internal", expected: "//a/b:__subpackages__"}, {rel: "a/b/internal/c", expected: "//a/b:__subpackages__"}, {rel: "a/b/internal/c/d", expected: "//a/b:__subpackages__"}, {rel: "a/b/internal/c/internal", expected: "//a/b/internal/c:__subpackages__"}, {rel: "a/b/internal/c/internal/d", expected: "//a/b/internal/c:__subpackages__"}, } for _, tt := range tests { if actual := CheckInternalVisibility(tt.rel, "default"); actual != tt.expected { t.Errorf("got %v; want %v", actual, tt.expected) } } } func TestSortLoadsByName(t *testing.T) { f, err := LoadMacroData( filepath.Join("third_party", "repos.bzl"), "", "repos", []byte(`load("@bazel_gazelle//:deps.bzl", "go_repository") load("//some:maybe.bzl", "maybe") load("//some2:maybe.bzl", "maybe2") load("//some1:maybe4.bzl", "maybe1") load("b.bzl", "x_library") def repos(): go_repository( name = "com_github_bazelbuild_bazel_gazelle", ) `)) if err != nil { t.Error(err) } sort.Stable(loadsByName{ loads: f.Loads, exprs: f.File.Stmt, }) f.Sync() got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(` load("@bazel_gazelle//:deps.bzl", "go_repository") load("//some:maybe.bzl", "maybe") load("//some1:maybe4.bzl", "maybe1") load("//some2:maybe.bzl", "maybe2") load("b.bzl", "x_library") def repos(): go_repository( name = "com_github_bazelbuild_bazel_gazelle", ) `) if got != want { t.Errorf("got:\n%s\nwant:%s", got, want) } } func TestSortRulesByKindAndName(t *testing.T) { f, err := LoadMacroData( filepath.Join("third_party", "repos.bzl"), "", "repos", []byte(`load("@bazel_gazelle//:deps.bzl", "go_repository") def repos(): some_other_call_rule() go_repository( name = "com_github_andybalholm_cascadia", ) go_repository( name = "com_github_bazelbuild_buildtools", ) go_repository( name = "com_github_bazelbuild_rules_go", ) go_repository( name = "com_github_bazelbuild_bazel_gazelle", ) a_rule( name = "name1", ) `)) if err != nil { t.Error(err) } sort.Stable(rulesByKindAndName{ rules: f.Rules, exprs: f.function.stmt.Body, }) repos := []string{ "name1", "com_github_andybalholm_cascadia", "com_github_bazelbuild_bazel_gazelle", "com_github_bazelbuild_buildtools", "com_github_bazelbuild_rules_go", } for i, r := range repos { rule := f.Rules[i] if rule.Name() != r { t.Errorf("expect rule %s at %d, got %s", r, i, rule.Name()) } if rule.Index() != i { t.Errorf("expect rule %s with index %d, got %d", r, i, rule.Index()) } if f.function.stmt.Body[i] != rule.expr { t.Errorf("underlying syntax tree of rule %s not sorted", r) } } got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(` load("@bazel_gazelle//:deps.bzl", "go_repository") def repos(): a_rule( name = "name1", ) go_repository( name = "com_github_andybalholm_cascadia", ) go_repository( name = "com_github_bazelbuild_bazel_gazelle", ) go_repository( name = "com_github_bazelbuild_buildtools", ) go_repository( name = "com_github_bazelbuild_rules_go", ) some_other_call_rule() `) if got != want { t.Errorf("got:%s\nwant:%s", got, want) } } func TestCheckFile(t *testing.T) { f := File{Rules: []*Rule{ NewRule("go_repository", "com_google_cloud_go_pubsub"), NewRule("go_repository", "com_google_cloud_go_pubsub"), }} err := checkFile(&f) if err == nil { t.Errorf("muliple rules with the same name should not be tolerated") } f = File{Rules: []*Rule{ NewRule("go_rules_dependencies", ""), NewRule("go_register_toolchains", ""), }} err = checkFile(&f) if err != nil { t.Errorf("unexpected error: %v", err) } } func TestAttributeComment(t *testing.T) { f, err := LoadData(filepath.Join("old", "BUILD.bazel"), "", nil) if err != nil { t.Fatal(err) } r := NewRule("a_rule", "name1") r.SetAttr("deps", []string{"foo", "bar", "baz"}) r.SetAttr("hdrs", []string{"foo", "bar", "baz"}) hdrComments := r.AttrComments("hdrs") hdrComments.Before = append(hdrComments.Before, bzl.Comment{ Token: "# do not sort", }) r.Insert(f) got := strings.TrimSpace(string(f.Format())) want := strings.TrimSpace(` a_rule( name = "name1", # do not sort hdrs = [ "foo", "bar", "baz", ], deps = [ "bar", "baz", "foo", ], ) `) if got != want { t.Errorf("got:%s\nwant:%s", got, want) } } func TestSimpleArgument(t *testing.T) { f := EmptyFile("foo", "bar") r := NewRule("export_files", "") r.AddArg(&bzl.CallExpr{ X: &bzl.Ident{Name: "glob"}, List: []bzl.Expr{ &bzl.ListExpr{ List: []bzl.Expr{ &bzl.StringExpr{Value: "**"}, }, }, }, }) r.Insert(f) f.Sync() got := strings.TrimSpace(string(bzl.FormatWithoutRewriting(f.File))) want := strings.TrimSpace(` export_files(glob(["**"])) `) if got != want { t.Errorf("got:%s\nwant:%s", got, want) } } func TestAttributeValueSorting(t *testing.T) { f := EmptyFile("foo", "bar") r := NewRule("a_rule", "") r.SetAttr("deps", []string{"foo", "bar", "baz"}) r.SetAttr("srcs", UnsortedStrings{"foo", "bar", "baz"}) r.SetAttr("hdrs", []string{"foo", "bar", "baz"}) r.Insert(f) f.Sync() got := strings.TrimSpace(string(bzl.FormatWithoutRewriting(f.File))) want := strings.TrimSpace(` a_rule( srcs = [ "foo", "bar", "baz", ], hdrs = [ "foo", "bar", "baz", ], deps = [ "bar", "baz", "foo", ], ) `) if got != want { t.Errorf("got:%s\nwant:%s", got, want) } } func TestAttributeValueSortingOverride(t *testing.T) { f := EmptyFile("foo", "bar") r := NewRule("a_rule", "") r.SetAttr("deps", []string{"foo", "bar", "baz"}) r.SetAttr("srcs", UnsortedStrings{"foo", "bar", "baz"}) r.SetAttr("hdrs", []string{"foo", "bar", "baz"}) r.SetSortedAttrs([]string{"srcs", "hdrs"}) r.Insert(f) f.Sync() got := strings.TrimSpace(string(bzl.FormatWithoutRewriting(f.File))) want := strings.TrimSpace(` a_rule( srcs = [ "foo", "bar", "baz", ], hdrs = [ "bar", "baz", "foo", ], deps = [ "foo", "bar", "baz", ], ) `) if got != want { t.Errorf("got:%s\nwant:%s", got, want) } if !reflect.DeepEqual(r.SortedAttrs(), []string{"srcs", "hdrs"}) { t.Errorf("Unexpected r.SortedAttrs(): %v", r.SortedAttrs()) } }