| /* |
| Copyright 2019 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" |
| "strings" |
| "testing" |
| |
| "github.com/bazelbuild/buildtools/build" |
| "github.com/google/go-cmp/cmp" |
| ) |
| |
| var removeCommentTests = []struct { |
| args []string |
| buildFile string |
| expected string |
| }{ |
| {[]string{}, |
| `# comment |
| foo( |
| name = "foo", |
| )`, |
| `foo( |
| name = "foo", |
| )`, |
| }, |
| {[]string{ |
| "name", |
| }, |
| `foo( |
| # comment |
| name = "foo", |
| )`, |
| `foo( |
| name = "foo", |
| )`, |
| }, |
| {[]string{ |
| "name", |
| }, |
| `foo( |
| name = "foo" # comment, |
| )`, |
| `foo( |
| name = "foo", |
| )`, |
| }, |
| {[]string{ |
| "deps", "bar", |
| }, |
| `foo( |
| name = "foo", |
| deps = [ |
| # comment |
| "bar", |
| "baz", |
| ], |
| )`, |
| `foo( |
| name = "foo", |
| deps = [ |
| "bar", |
| "baz", |
| ], |
| )`, |
| }, |
| {[]string{ |
| "deps", "bar", |
| }, |
| `foo( |
| name = "foo", |
| deps = [ |
| "bar", # comment |
| "baz", |
| ], |
| )`, |
| `foo( |
| name = "foo", |
| deps = [ |
| "bar", |
| "baz", |
| ], |
| )`, |
| }, |
| } |
| |
| func TestCmdRemoveComment(t *testing.T) { |
| for i, tt := range removeCommentTests { |
| bld, err := build.Parse("BUILD", []byte(tt.buildFile)) |
| if err != nil { |
| t.Error(err) |
| continue |
| } |
| rl := bld.Rules("foo")[0] |
| env := CmdEnvironment{ |
| File: bld, |
| Rule: rl, |
| Args: tt.args, |
| } |
| bld, _ = cmdRemoveComment(NewOpts(), env) |
| got := strings.TrimSpace(string(build.Format(bld))) |
| if got != tt.expected { |
| t.Errorf("cmdRemoveComment(%d):\ngot:\n%s\nexpected:\n%s", i, got, tt.expected) |
| } |
| } |
| } |
| |
| type targetExpressionToBuildFilesTestCase struct { |
| rootDir, target string |
| buildFiles []string |
| } |
| |
| func setupTestTmpWorkspace(t *testing.T, buildFileName string) (tmp string) { |
| tmp, err := os.MkdirTemp("", "") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // On MacOS "/tmp" is a symlink to "/private/tmp". Resolve it to make the testing easier |
| tmp, err = filepath.EvalSymlinks(tmp) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if err := os.MkdirAll(filepath.Join(tmp, "a", "b"), 0755); err != nil { |
| t.Fatal(err) |
| } |
| if err := os.MkdirAll(filepath.Join(tmp, "a", "c"), 0755); err != nil { |
| t.Fatal(err) |
| } |
| // Create additional directories that will be ignored |
| if err := os.MkdirAll(filepath.Join(tmp, "ignored"), 0755); err != nil { |
| t.Fatal(err) |
| } |
| if err := os.MkdirAll(filepath.Join(tmp, "a", "ignored"), 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) |
| } |
| if err := os.WriteFile(filepath.Join(tmp, "a", "c", buildFileName), nil, 0755); err != nil { |
| t.Fatal(err) |
| } |
| // Create BUILD files in ignored directories to verify they're skipped |
| if err := os.WriteFile(filepath.Join(tmp, "ignored", buildFileName), nil, 0755); err != nil { |
| t.Fatal(err) |
| } |
| if err := os.WriteFile(filepath.Join(tmp, "a", "ignored", buildFileName), nil, 0755); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Create .bazelignore file with paths to ignore |
| bazelignoreContent := []byte("# Ignore these directories\nignored\na/ignored/\n") |
| if err := os.WriteFile(filepath.Join(tmp, ".bazelignore"), bazelignoreContent, 0644); err != nil { |
| t.Fatal(err) |
| } |
| |
| return |
| } |
| |
| func runTestTargetExpressionToBuildFiles(t *testing.T, buildFileName string) { |
| tmp := setupTestTmpWorkspace(t, buildFileName) |
| defer os.RemoveAll(tmp) |
| |
| for _, tc := range []targetExpressionToBuildFilesTestCase{ |
| {tmp, "//", []string{filepath.Join(tmp, buildFileName)}}, |
| {tmp, "//:foo", []string{filepath.Join(tmp, buildFileName)}}, |
| {tmp, "//a", []string{filepath.Join(tmp, "a", buildFileName)}}, |
| {tmp, "//a:foo", []string{filepath.Join(tmp, "a", buildFileName)}}, |
| {tmp, "//a/b", []string{filepath.Join(tmp, "a", "b", buildFileName)}}, |
| {tmp, "//a/b:foo", []string{filepath.Join(tmp, "a", "b", buildFileName)}}, |
| {tmp, "//...", []string{filepath.Join(tmp, buildFileName), filepath.Join(tmp, "a", buildFileName), filepath.Join(tmp, "a", "b", buildFileName), filepath.Join(tmp, "a", "c", buildFileName)}}, |
| {tmp, "//a/...", []string{filepath.Join(tmp, "a", buildFileName), filepath.Join(tmp, "a", "b", buildFileName), filepath.Join(tmp, "a", "c", buildFileName)}}, |
| {tmp, "//a/b/...", []string{filepath.Join(tmp, "a", "b", buildFileName)}}, |
| {tmp, "//a/c/...", []string{filepath.Join(tmp, "a", "c", buildFileName)}}, |
| {tmp, "//a/c/...:foo", []string{filepath.Join(tmp, "a", "c", buildFileName)}}, |
| {"", "...:foo", []string{filepath.Join(tmp, buildFileName), filepath.Join(tmp, "a", buildFileName), filepath.Join(tmp, "a", "b", buildFileName), filepath.Join(tmp, "a", "c", buildFileName)}}, |
| } { |
| if tc.rootDir == "" { |
| cwd, err := os.Getwd() |
| if err != nil { |
| t.Fatal(err) |
| } |
| // buildozer should be able to find the WORKSPACE file in the current wd |
| if err := os.Chdir(tmp); err != nil { |
| t.Fatal(err) |
| } |
| defer os.Chdir(cwd) |
| } |
| |
| buildFiles := targetExpressionToBuildFiles(tc.rootDir, tc.target, true) |
| expectedBuildFilesMap := make(map[string]bool) |
| buildFilesMap := make(map[string]bool) |
| for _, buildFile := range buildFiles { |
| buildFilesMap[buildFile] = true |
| } |
| for _, buildFile := range tc.buildFiles { |
| expectedBuildFilesMap[buildFile] = true |
| } |
| if !reflect.DeepEqual(expectedBuildFilesMap, buildFilesMap) { |
| t.Errorf("TargetExpressionToBuildFiles(%q, %q) = %q want %q", tc.rootDir, tc.target, buildFiles, tc.buildFiles) |
| } |
| } |
| } |
| |
| func TestTargetExpressionToBuildFiles(t *testing.T) { |
| for _, buildFileName := range BuildFileNames { |
| runTestTargetExpressionToBuildFiles(t, buildFileName) |
| } |
| } |
| |
| func runTestAppendCommands(t *testing.T, buildFileName string) { |
| tmp := setupTestTmpWorkspace(t, buildFileName) |
| defer os.RemoveAll(tmp) |
| |
| for _, tc := range []targetExpressionToBuildFilesTestCase{ |
| {tmp, ".:__pkg__", []string{"./" + buildFileName}}, |
| {tmp, "a" + ":__pkg__", []string{"a/" + buildFileName}}, |
| {"", "a" + ":__pkg__", []string{"a/" + buildFileName}}, |
| } { |
| if tc.rootDir == "" { |
| cwd, err := os.Getwd() |
| if err != nil { |
| t.Fatal(err) |
| } |
| // buildozer should be able to find the WORKSPACE file in the current wd |
| if err := os.Chdir(tmp); err != nil { |
| t.Fatal(err) |
| } |
| defer os.Chdir(cwd) |
| } |
| |
| commandsByFile := make(map[string][]commandsForTarget) |
| opts := NewOpts() |
| opts.RootDir = tc.rootDir |
| appendCommands(opts, commandsByFile, tc.buildFiles) |
| if len(commandsByFile) != 1 { |
| t.Errorf("Expect one target after appendCommands") |
| } |
| for _, value := range commandsByFile { |
| if value[0].target != tc.target { |
| t.Errorf("appendCommands for buildfile %s yielded target %s, expected %s", tc.buildFiles, value[0].target, tc.target) |
| } |
| } |
| } |
| } |
| |
| func TestAppendCommands(t *testing.T) { |
| for _, buildFileName := range BuildFileNames { |
| runTestAppendCommands(t, buildFileName) |
| } |
| } |
| |
| var dictListAddTests = []struct { |
| args []string |
| buildFile string |
| expected string |
| }{ |
| {[]string{ |
| "attr", "key1", "value1", |
| }, |
| `foo( |
| name = "foo", |
| )`, |
| `foo( |
| name = "foo", |
| attr = {"key1": ["value1"]}, |
| )`, |
| }, |
| {[]string{ |
| "attr", "key1", "value2", |
| }, |
| `foo( |
| name = "foo", |
| attr = {"key1": ["value1"]}, |
| )`, |
| `foo( |
| name = "foo", |
| attr = {"key1": [ |
| "value1", |
| "value2", |
| ]}, |
| )`, |
| }, |
| {[]string{ |
| "attr", "key1", "value1", "value2", |
| }, |
| `foo( |
| name = "foo", |
| )`, |
| `foo( |
| name = "foo", |
| attr = {"key1": [ |
| "value1", |
| "value2", |
| ]}, |
| )`, |
| }, |
| {[]string{ |
| "attr", "key2", "value2", |
| }, |
| `foo( |
| name = "foo", |
| attr = {"key1": ["value1"]}, |
| )`, |
| `foo( |
| name = "foo", |
| attr = { |
| "key1": ["value1"], |
| "key2": ["value2"], |
| }, |
| )`, |
| }, |
| {[]string{ |
| "attr", "key1", "value1", |
| }, |
| `foo( |
| name = "foo", |
| attr = {"key1": ["value1"]}, |
| )`, |
| `foo( |
| name = "foo", |
| attr = {"key1": ["value1"]}, |
| )`, |
| }, |
| } |
| |
| func TestCmdDictListAdd(t *testing.T) { |
| for i, tt := range dictListAddTests { |
| bld, err := build.Parse("BUILD", []byte(tt.buildFile)) |
| if err != nil { |
| t.Error(err) |
| continue |
| } |
| rl := bld.Rules("foo")[0] |
| env := CmdEnvironment{ |
| File: bld, |
| Rule: rl, |
| Args: tt.args, |
| } |
| bld, _ = cmdDictListAdd(NewOpts(), env) |
| got := strings.TrimSpace(string(build.Format(bld))) |
| if got != tt.expected { |
| t.Errorf("cmdDictListAdd(%d):\ngot:\n%s\nexpected:\n%s", i, got, tt.expected) |
| } |
| } |
| } |
| |
| var dictReplaceIfEqualTests = []struct { |
| args []string |
| buildFile string |
| expected string |
| }{ |
| {[]string{ |
| "attr", "key1", "value1", "value2", |
| }, |
| `foo( |
| name = "foo", |
| attr = {"key1": "value1"}, |
| )`, |
| `foo( |
| name = "foo", |
| attr = {"key1": "value2"}, |
| )`, |
| }, |
| {[]string{ |
| "attr", "key1", "value1", "value2", |
| }, |
| `foo( |
| name = "foo", |
| attr = {"key1": "x"}, |
| )`, |
| `foo( |
| name = "foo", |
| attr = {"key1": "x"}, |
| )`, |
| }, |
| {[]string{ |
| "attr", "key1", "value1", "value2", |
| }, |
| `foo( |
| name = "foo", |
| attr = { |
| "key1": ["value1"], |
| "key2": ["value2"], |
| }, |
| )`, |
| `foo( |
| name = "foo", |
| attr = { |
| "key1": ["value1"], |
| "key2": ["value2"], |
| }, |
| )`, |
| }, |
| {[]string{ |
| "attr", "key1", "value1", "value2", |
| }, |
| `foo( |
| name = "foo", |
| attr = { |
| "key1": "value1", |
| "key2": "value2", |
| }, |
| )`, |
| `foo( |
| name = "foo", |
| attr = { |
| "key1": "value2", |
| "key2": "value2", |
| }, |
| )`, |
| }, |
| {[]string{ |
| "attr", "key1", "value1", "value2", |
| }, |
| `foo( |
| name = "foo", |
| attr = { |
| "key1": "value1", |
| "key2": "x", |
| }, |
| )`, |
| `foo( |
| name = "foo", |
| attr = { |
| "key1": "value2", |
| "key2": "x", |
| }, |
| )`, |
| }, |
| } |
| |
| func TestCmdDictReplaceIfEqual(t *testing.T) { |
| for i, tt := range dictReplaceIfEqualTests { |
| bld, err := build.Parse("BUILD", []byte(tt.buildFile)) |
| if err != nil { |
| t.Error(err) |
| continue |
| } |
| expectedBld, err := build.Parse("BUILD", []byte(tt.expected)) |
| if err != nil { |
| t.Error(err) |
| continue |
| } |
| rl := bld.Rules("foo")[0] |
| env := CmdEnvironment{ |
| File: bld, |
| Rule: rl, |
| Args: tt.args, |
| } |
| bld, err = cmdDictReplaceIfEqual(NewOpts(), env) |
| if err != nil { |
| t.Errorf("cmdDictReplaceIfEqual(%d):\ngot error:\n%s", i, err) |
| } |
| got := strings.TrimSpace(string(build.Format(bld))) |
| expected := strings.TrimSpace(string(build.Format(expectedBld))) |
| if got != expected { |
| t.Errorf("cmdDictReplaceIfEqual(%d):\ngot:\n%s\nexpected:\n%s", i, got, tt.expected) |
| } |
| } |
| } |
| |
| var substituteLoadsTests = []struct { |
| args []string |
| buildFile string |
| expected string |
| }{ |
| {[]string{ |
| "^(.*)$", "${1}", |
| }, |
| `load("//foo:foo.bzl", "foo")`, |
| `load("//foo:foo.bzl", "foo")`, |
| }, |
| {[]string{ |
| "^@rules_foo//foo:defs.bzl$", "//build/rules/foo:defs.bzl", |
| }, |
| `load("@rules_bar//bar:defs.bzl", "bar")`, |
| `load("@rules_bar//bar:defs.bzl", "bar")`, |
| }, |
| {[]string{ |
| "^@rules_foo//foo:defs.bzl$", "//build/rules/foo:defs.bzl", |
| }, |
| `load("@rules_foo//foo:defs.bzl", "foo", "foo2") |
| load("@rules_bar//bar:defs.bzl", "bar")`, |
| `load("@rules_bar//bar:defs.bzl", "bar") |
| load("//build/rules/foo:defs.bzl", "foo", "foo2")`, |
| }, |
| {[]string{ |
| ":foo.bzl$", ":defs.bzl", |
| }, |
| `load("//foo:foo.bzl", "foo")`, |
| `load("//foo:defs.bzl", "foo")`, |
| }, |
| {[]string{ |
| // Keep in sync with the example in `//buildozer:README.md`. |
| "^@([^/]*)//([^:].*)$", "//third_party/build_defs/${1}/${2}", |
| }, |
| `load("@rules_foo//foo:defs.bzl", "foo", "foo2") |
| load("@rules_bar//bar:defs.bzl", "bar") |
| load("@rules_bar//:defs.bzl", legacy_bar = "bar")`, |
| `load("@rules_bar//:defs.bzl", legacy_bar = "bar") |
| load("//third_party/build_defs/rules_bar/bar:defs.bzl", "bar") |
| load("//third_party/build_defs/rules_foo/foo:defs.bzl", "foo", "foo2")`, |
| }, |
| } |
| |
| func TestCmdSubstituteLoad(t *testing.T) { |
| for i, tt := range substituteLoadsTests { |
| bld, err := build.Parse("BUILD", []byte(tt.buildFile)) |
| if err != nil { |
| t.Error(err) |
| continue |
| } |
| env := CmdEnvironment{ |
| File: bld, |
| Args: tt.args, |
| } |
| bld, _ = cmdSubstituteLoad(NewOpts(), env) |
| got := strings.TrimSpace(string(build.Format(bld))) |
| if got != tt.expected { |
| t.Errorf("cmdSubstituteLoad(%d):\ngot:\n%s\nexpected:\n%s", i, got, tt.expected) |
| } |
| } |
| } |
| |
| func TestCmdSubstitute(t *testing.T) { |
| for i, tc := range []struct { |
| name string |
| args []string |
| buildFile string |
| expected string |
| }{ |
| { |
| name: "empty_rule", |
| args: []string{"*", "^$", "x"}, |
| buildFile: `cc_library()`, |
| expected: `cc_library()`, |
| }, |
| { |
| name: "known_attr", |
| args: []string{"*", "^//(.*)$", "//foo/${1}"}, |
| buildFile: `cc_library(deps = ["//bar/baz:quux"])`, |
| expected: `cc_library(deps = ["//foo/bar/baz:quux"])`, |
| }, |
| { |
| name: "custom_attr", |
| args: []string{"*", "^//(.*)$", "//foo/${1}"}, |
| buildFile: `cc_library(my_custom_attr = "//bar/baz:quux")`, |
| expected: `cc_library(my_custom_attr = "//foo/bar/baz:quux")`, |
| }, |
| { |
| name: "specific_rule", |
| args: []string{"deps", "^//(.*)$", "//foo/${1}"}, |
| buildFile: `cc_library(deps = ["//bar"], fancy_deps = ["//bar/baz:quux"])`, |
| expected: `cc_library( |
| fancy_deps = ["//bar/baz:quux"], |
| deps = ["//foo/bar"], |
| )`, |
| }, |
| } { |
| t.Run(tc.name, func(t *testing.T) { |
| bld, err := build.Parse("BUILD", []byte(tc.buildFile)) |
| if err != nil { |
| t.Error(err) |
| return |
| } |
| env := CmdEnvironment{ |
| File: bld, |
| Args: tc.args, |
| Rule: bld.RuleAt(1), |
| } |
| bld, _ = cmdSubstitute(NewOpts(), env) |
| got := strings.TrimSpace(string(build.Format(bld))) |
| if got != tc.expected { |
| t.Errorf("cmdSubstitute(%d):\ngot:\n%s\nexpected:\n%s", i, got, tc.expected) |
| } |
| }) |
| } |
| } |
| |
| func TestCmdDictAddSet_missingColon(t *testing.T) { |
| for _, tc := range []struct { |
| name string |
| fun func(*Options, CmdEnvironment) (*build.File, error) |
| }{ |
| {"dict_add", cmdDictAdd}, |
| {"dict_set", cmdDictSet}, |
| } { |
| t.Run(tc.name, func(t *testing.T) { |
| bld, err := build.Parse("BUILD", []byte("rule()")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| env := CmdEnvironment{ |
| File: bld, |
| Rule: bld.RuleAt(1), |
| Args: []string{"attr", "invalid"}, |
| } |
| _, err = tc.fun(NewOpts(), env) |
| if err == nil { |
| t.Error("succeeded, want error") |
| } |
| }) |
| } |
| } |
| |
| func TestCmdDictOperations(t *testing.T) { |
| tests := []struct { |
| name string |
| dictAddArgs []string |
| dictSetArgs []string |
| dictRemoveArgs []string |
| input string |
| want string |
| }{ |
| { |
| name: "dict_add_set_remove", |
| dictAddArgs: []string{"dict_attr", `added_entry:new_value`}, |
| dictSetArgs: []string{"dict_attr", `entry_to_change:updated_value`}, |
| dictRemoveArgs: []string{"dict_attr", `entry_to_remove`}, |
| input: strings.Join([]string{ |
| `rule(`, |
| ` name = "rule_name",`, |
| ` dict_attr = {`, |
| ` "entry_to_change": "123",`, |
| ` "entry_to_remove": "abc",`, |
| ` },`, |
| `)`, |
| }, "\n"), |
| want: strings.Join([]string{ |
| `rule(`, |
| ` name = "rule_name",`, |
| ` dict_attr = {`, |
| ` "entry_to_change": "updated_value",`, |
| ` "added_entry": "new_value",`, |
| ` },`, |
| `)`, |
| ``, |
| }, "\n"), |
| }, |
| { |
| name: "dict_add_set_remove_with_escaped_colon", |
| dictAddArgs: []string{"dict_attr", `added\:entry:new:value`}, |
| dictSetArgs: []string{"dict_attr", `entry\:to_change:updated\:value`}, |
| dictRemoveArgs: []string{"dict_attr", `entry\:to_remove`}, |
| input: strings.Join([]string{ |
| `rule(`, |
| ` name = "rule_name",`, |
| ` dict_attr = {`, |
| ` "entry:to_change": "123",`, |
| ` "entry:to_remove": "abc",`, |
| ` },`, |
| `)`, |
| }, "\n"), |
| want: strings.Join([]string{ |
| `rule(`, |
| ` name = "rule_name",`, |
| ` dict_attr = {`, |
| ` "entry:to_change": "updated:value",`, |
| ` "added:entry": "new:value",`, |
| ` },`, |
| `)`, |
| ``, |
| }, "\n"), |
| }, |
| } |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| file, err := build.Parse("BUILD", []byte(tc.input)) |
| if err != nil { |
| t.Fatalf("build.Parse returned err: %s", err) |
| } |
| rule := file.RuleNamed("rule_name") |
| file, err = cmdDictAdd(NewOpts(), CmdEnvironment{File: file, Rule: rule, Args: tc.dictAddArgs}) |
| if err != nil { |
| t.Fatalf("cmdDictAdd returned err: %s", err) |
| } |
| |
| file, err = cmdDictSet(NewOpts(), CmdEnvironment{File: file, Rule: rule, Args: tc.dictSetArgs}) |
| if err != nil { |
| t.Fatalf("cmdDictSet returned err: %s", err) |
| } |
| |
| file, err = cmdDictRemove(NewOpts(), CmdEnvironment{File: file, Rule: rule, Args: tc.dictRemoveArgs}) |
| if err != nil { |
| t.Fatalf("cmdDictRemove returned err: %s", err) |
| } |
| |
| got := string(build.Format(file)) |
| |
| if diff := cmp.Diff(tc.want, got); diff != "" { |
| t.Fatalf("dict operations returned diff -want +got %v", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestCmdSetSelect(t *testing.T) { |
| for i, tc := range []struct { |
| name string |
| args []string |
| buildFile string |
| expected string |
| }{ |
| { |
| name: "select_statement_doesn't_exist", |
| args: []string{ |
| "args", /* attr */ |
| ":use_ci_timeouts", "-test.timeout=123s", /* key, value */ |
| ":use_ci_timeouts", "-test.anotherFlag=flagValue", /* key, value */ |
| "//conditions:default", "-test.timeout=789s", /* key, value */ |
| }, |
| buildFile: `foo( |
| name = "foo", |
| )`, |
| expected: `foo( |
| name = "foo", |
| args = select({ |
| ":use_ci_timeouts": [ |
| "-test.timeout=123s", |
| "-test.anotherFlag=flagValue", |
| ], |
| "//conditions:default": ["-test.timeout=789s"], |
| }), |
| )`}, |
| { |
| name: "select_statement_exists", |
| args: []string{ |
| "args", /* attr */ |
| ":use_ci_timeouts", "-test.timeout=543s", /* key, value */ |
| "//conditions:default", "-test.timeout=876s", /* key, value */ |
| }, |
| buildFile: `foo( |
| name = "foo", |
| args = select({ |
| ":use_ci_timeouts": [ |
| "-test.timeout=123s", |
| "-test.anotherFlag=flagValue", |
| ], |
| "//conditions:default": ["-test.timeout=789s"], |
| }), |
| )`, |
| expected: `foo( |
| name = "foo", |
| args = select({ |
| ":use_ci_timeouts": ["-test.timeout=543s"], |
| "//conditions:default": ["-test.timeout=876s"], |
| }), |
| )`}, |
| { |
| name: "attr_exists_but_not_select", |
| args: []string{ |
| "args", /* attr */ |
| ":use_ci_timeouts", "-test.timeout=543s", /* key, value */ |
| "//conditions:default", "-test.timeout=876s", /* key, value */ |
| }, |
| buildFile: `foo( |
| name = "foo", |
| args = ["-test.timeout=123s"], |
| )`, |
| expected: `foo( |
| name = "foo", |
| args = select({ |
| ":use_ci_timeouts": ["-test.timeout=543s"], |
| "//conditions:default": ["-test.timeout=876s"], |
| }), |
| )`}, |
| } { |
| t.Run(tc.name, func(t *testing.T) { |
| bld, err := build.Parse("BUILD", []byte(tc.buildFile)) |
| if err != nil { |
| t.Error(err) |
| } |
| rl := bld.Rules("foo")[0] |
| env := CmdEnvironment{ |
| File: bld, |
| Rule: rl, |
| Args: tc.args, |
| } |
| bld, _ = cmdSetSelect(NewOpts(), env) |
| got := strings.TrimSpace(string(build.Format(bld))) |
| if got != tc.expected { |
| t.Errorf("cmdSetSelect(%d):\ngot:\n%s\nexpected:\n%s", i, got, tc.expected) |
| } |
| }) |
| } |
| } |
| |
| func TestExecuteCommandsOnInlineFile(t *testing.T) { |
| tests := []struct { |
| name string |
| fileContent []byte |
| commands []string |
| wantOutput []byte |
| }{ |
| { |
| name: "creating_new_target_and_adding_deps", |
| fileContent: nil, |
| commands: []string{ |
| "new java_library foo|//package/path/BUILD", |
| "add deps :bar|//package/path:foo", |
| }, |
| wantOutput: []byte(strings.Join([]string{ |
| `java_library(`, |
| ` name = "foo",`, |
| ` deps = [":bar"],`, |
| `)`, |
| ``}, "\n")), |
| }, |
| { |
| name: "adding_deps_to_existing_targets", |
| fileContent: []byte(strings.Join([]string{ |
| `java_library(`, |
| ` name = "foo",`, |
| `)`, |
| ``, |
| `java_library(`, |
| ` name = "fruits",`, |
| ` deps = ["//package/fruits:apples"],`, |
| `)`, |
| ``}, "\n")), |
| commands: []string{ |
| "add deps :bar|//package/path:foo", |
| "add deps //package/fruits:oranges|//package/path:fruits", |
| }, |
| wantOutput: []byte(strings.Join([]string{ |
| `java_library(`, |
| ` name = "foo",`, |
| ` deps = [":bar"],`, |
| `)`, |
| ``, |
| `java_library(`, |
| ` name = "fruits",`, |
| ` deps = [`, |
| ` "//package/fruits:apples",`, |
| ` "//package/fruits:oranges",`, |
| ` ],`, |
| `)`, |
| ``}, "\n")), |
| }, |
| { |
| name: "substituting_a_target", |
| fileContent: []byte(strings.Join([]string{ |
| `java_library(`, |
| ` name = "fruits",`, |
| ` deps = ["//package/fruits:apples"],`, |
| `)`, |
| ``}, "\n")), |
| commands: []string{ |
| "replace deps //package/fruits:apples //package/fruits:oranges|//whatever/package/path:fruits", |
| }, |
| wantOutput: []byte(strings.Join([]string{ |
| `java_library(`, |
| ` name = "fruits",`, |
| ` deps = ["//package/fruits:oranges"],`, |
| `)`, |
| ``}, "\n")), |
| }, |
| { |
| name: "no_changes_does_not_return_any_diff", |
| fileContent: []byte(strings.Join([]string{ |
| `java_library(`, |
| ` name = "foo",`, |
| ` deps = [":bar"],`, |
| `)`, |
| ``}, "\n")), |
| commands: []string{ |
| "add deps :bar |//whatever/package/path:foo", |
| }, |
| wantOutput: []byte(strings.Join([]string{ |
| `java_library(`, |
| ` name = "foo",`, |
| ` deps = [":bar"],`, |
| `)`, |
| ``}, "\n")), |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| output, err := ExecuteCommandsOnInlineFile(tc.fileContent, tc.commands) |
| if err != nil { |
| t.Fatalf("Error, got error %v", err) |
| } |
| |
| if diff := cmp.Diff(tc.wantOutput, output); diff != "" { |
| t.Errorf("%s: (-want +got): %s", tc.name, diff) |
| } |
| }) |
| } |
| } |
| |
| func TestTestExecuteCommandsOnInlineFileFailed(t *testing.T) { |
| tests := []struct { |
| name string |
| fileContent []byte |
| commands []string |
| wantErr error |
| }{ |
| { |
| name: "target_does_not_exist", |
| commands: []string{ |
| "add deps :foo|//package/path:bar", |
| }, |
| wantErr: fmt.Errorf("rule 'bar' not found"), |
| }, |
| { |
| name: "invalid_input", |
| commands: []string{ |
| "completely invalid command", |
| }, |
| wantErr: fmt.Errorf("rule 'completely invalid command' not found"), |
| }, |
| { |
| name: "missing_implementation", |
| commands: []string{ |
| "extrapolate packages :foo|//package/path:bar", |
| }, |
| wantErr: fmt.Errorf("invalid input commands, expected all commands to reference a single file"), |
| }, |
| { |
| name: "commands_for_multiple_files", |
| commands: []string{ |
| "add deps :foo|//package/path:bar", |
| "add deps :foo|//package2/path:bar", |
| }, |
| wantErr: fmt.Errorf("invalid input commands, expected all commands to reference a single file"), |
| }, |
| { |
| name: "command_with_unexpected_target_semicolons", |
| commands: []string{ |
| "add deps :foo|//package:path:bar", |
| }, |
| wantErr: fmt.Errorf("invalid target name \"//package:path:bar\""), |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| output, gotErr := ExecuteCommandsOnInlineFile(tc.fileContent, tc.commands) |
| |
| if output != nil { |
| t.Fatalf("Error, got response for invalid input %v, expected error", output) |
| } |
| |
| if diff := cmp.Diff(tc.wantErr.Error(), gotErr.Error()); diff != "" { |
| t.Errorf("%s: (-want +got): %s", tc.name, diff) |
| } |
| }) |
| } |
| } |
| |
| func TestGetIgnoredPrefixes(t *testing.T) { |
| tmp, err := os.MkdirTemp("", "") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(tmp) |
| |
| tests := []struct { |
| name string |
| bazelignore string |
| expected []string |
| expectError bool |
| errorContains string |
| }{ |
| { |
| name: "valid paths", |
| bazelignore: `# Ignore these directories |
| ignored |
| a/ignored |
| b/c/d`, |
| expected: []string{"ignored", "a/ignored", "b/c/d"}, |
| }, |
| { |
| name: "empty file", |
| bazelignore: ` `, |
| expected: []string{}, |
| }, |
| { |
| name: "only comments", |
| bazelignore: `# This is a comment |
| # Another comment`, |
| expected: []string{}, |
| }, |
| { |
| name: "empty lines", |
| bazelignore: `ignored |
| |
| a/ignored |
| |
| # comment |
| b/c/d`, |
| expected: []string{"ignored", "a/ignored", "b/c/d"}, |
| }, |
| { |
| name: "absolute paths", |
| bazelignore: `/absolute/path |
| ignored |
| /another/absolute/path`, |
| expected: []string{"ignored"}, |
| }, |
| { |
| name: "no file", |
| bazelignore: "", |
| expected: []string{}, |
| }, |
| { |
| name: "trailing slash should be normalized", |
| bazelignore: `ignored/`, |
| expected: []string{"ignored"}, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| // Create a new temporary directory for each test case |
| testDir, err := os.MkdirTemp(tmp, "") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if tt.bazelignore != "" { |
| if err := os.WriteFile(filepath.Join(testDir, ".bazelignore"), []byte(tt.bazelignore), 0644); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| got := getIgnoredPrefixes(testDir) |
| if !reflect.DeepEqual(got, tt.expected) { |
| t.Errorf("getIgnoredPrefixes() = %v, want %v", got, tt.expected) |
| } |
| }) |
| } |
| } |
| |
| func TestShouldIgnorePath(t *testing.T) { |
| tmp, err := os.MkdirTemp("", "") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(tmp) |
| |
| tests := []struct { |
| name string |
| path string |
| ignoredPrefixes []string |
| want bool |
| }{ |
| { |
| name: "exact match", |
| path: filepath.Join(tmp, "foo"), |
| ignoredPrefixes: []string{"foo"}, |
| want: true, |
| }, |
| { |
| name: "subdirectory", |
| path: filepath.Join(tmp, "foo", "bar"), |
| ignoredPrefixes: []string{"foo"}, |
| want: true, |
| }, |
| { |
| name: "similar prefix but not directory", |
| path: filepath.Join(tmp, "foobar"), |
| ignoredPrefixes: []string{"foo"}, |
| want: false, // Should not ignore "foobar" when only "foo" is ignored |
| }, |
| { |
| name: "matched with multiple prefixes", |
| path: filepath.Join(tmp, "foobar"), |
| ignoredPrefixes: []string{"foo2", "foobar", "baz"}, |
| want: true, |
| }, |
| { |
| name: "no match", |
| path: filepath.Join(tmp, "bar"), |
| ignoredPrefixes: []string{"foo", "baz"}, |
| want: false, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| got := shouldIgnorePath(tt.path, tmp, tt.ignoredPrefixes) |
| if got != tt.want { |
| t.Errorf("shouldIgnorePath(%q, %q, %v) = %v, want %v", |
| tt.path, tmp, tt.ignoredPrefixes, got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestSplitOnNonEscaped(t *testing.T) { |
| tests := []struct { |
| name string |
| input string |
| sep byte |
| lim int |
| want []string |
| }{ |
| { |
| name: "no_split", |
| input: "one:two", |
| sep: '|', |
| lim: -1, |
| want: []string{"one:two"}, |
| }, |
| { |
| name: "split_to_two", |
| input: "one:two", |
| sep: ':', |
| lim: 2, |
| want: []string{"one", "two"}, |
| }, |
| { |
| name: "split_with_limit", |
| input: "one:two:three:four", |
| sep: ':', |
| lim: 2, |
| want: []string{"one", "two:three:four"}, |
| }, |
| { |
| name: "split_without_limit", |
| input: "one:two:three:four", |
| sep: ':', |
| lim: -1, |
| want: []string{"one", "two", "three", "four"}, |
| }, |
| { |
| name: "does_not_split_on_escaped", |
| input: `one\:two:three`, |
| sep: ':', |
| lim: 2, |
| want: []string{`one\:two`, "three"}, |
| }, |
| { |
| name: "split_on_pipe", |
| input: `one|two|three`, |
| sep: '|', |
| lim: -1, |
| want: []string{"one", "two", "three"}, |
| }, |
| { |
| name: "skip_escaped_pipes", |
| input: `one\|two|three`, |
| sep: '|', |
| lim: -1, |
| want: []string{`one\|two`, "three"}, |
| }, |
| { |
| name: "split_with_unicode", |
| input: `😉|❤️|😁`, |
| sep: '|', |
| lim: -1, |
| want: []string{"😉", "❤️", "😁"}, |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| got := splitOnNonEscaped(tc.input, tc.sep, tc.lim) |
| if diff := cmp.Diff(tc.want, got); diff != "" { |
| t.Fatalf("splitOnNonEscaped returned diff -want +got %v", diff) |
| } |
| }) |
| } |
| } |