blob: c1814a4292b52bfff8dd7acabc2dc98d24dce0ca [file] [log] [blame] [edit]
/*
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)
}
})
}
}