blob: bf60c8793c0f7693f65bac5e75cca87d78cc4702 [file] [log] [blame] [edit]
/*
Copyright 2020 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 warn
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/bazelbuild/buildtools/build"
"github.com/bazelbuild/buildtools/testutils"
)
const (
scopeBuild = build.TypeBuild
scopeBzl = build.TypeBzl
scopeWorkspace = build.TypeWorkspace
scopeDefault = build.TypeDefault
scopeModule = build.TypeModule
scopeEverywhere = scopeBuild | scopeBzl | scopeWorkspace | scopeDefault | scopeModule
scopeBazel = scopeBuild | scopeBzl | scopeWorkspace | scopeModule
scopeDeclarative = scopeBuild | scopeWorkspace | scopeModule
)
// A global FileReader object that can be used by tests. If a test redefines it it must
// reset it when it finishes.
var testFileReader *FileReader
// A global variable containing package name for test cases. Can be overwritten
// but must be reset when the test finishes.
var testPackage string = "test/package"
// fileReaderRequests is used by tests to check which files have actually been requested by testFileReader
var fileReaderRequests []string
func setUpFileReader(data map[string]string) (cleanup func()) {
readFile := func(filename string) ([]byte, error) {
fileReaderRequests = append(fileReaderRequests, filename)
if contents, ok := data[filename]; ok {
return []byte(contents), nil
}
return nil, fmt.Errorf("file not found")
}
testFileReader = NewFileReader(readFile)
fileReaderRequests = nil
// The cached mapping depends on the file reader, so we need to reset it as well.
moduleToApparentRepoName = nil
return func() {
// Tear down
testFileReader = nil
fileReaderRequests = nil
}
}
func setUpTestPackage(name string) (cleanup func()) {
oldName := testPackage
testPackage = name
return func() {
// Tear down
testPackage = oldName
}
}
func getFilename(fileType build.FileType) string {
switch fileType {
case build.TypeBuild:
return "BUILD"
case build.TypeWorkspace:
return "WORKSPACE"
case build.TypeBzl:
return "test_file.bzl"
case build.TypeModule:
return "MODULE.bazel"
default:
return "test_file.strlrk"
}
}
func getFileForTest(input string, fileType build.FileType) *build.File {
input = strings.TrimLeft(input, "\n")
filename := getFilename(fileType)
file, err := build.Parse(testPackage+"/"+filename, []byte(input))
if err != nil {
panic(fmt.Sprintf("%v", err))
}
file.Pkg = testPackage
file.Label = filename
file.WorkspaceRoot = "/home/users/foo/bar"
return file
}
func getFindings(categories, input string, fileType build.FileType) []*Finding {
file := getFileForTest(input, fileType)
return FileWarnings(file, strings.Split(categories, ","), nil, ModeWarn, testFileReader)
}
func compareFindings(t *testing.T, categories, input string, expected []string, scope, fileType build.FileType) {
// If scope doesn't match the file type, no warnings are expected
if scope&fileType == 0 {
expected = []string{}
}
findings := getFindings(categories, input, fileType)
// We ensure that there is the expected number of warnings.
// At the moment, we check only the line numbers.
if len(expected) != len(findings) {
t.Errorf("Input: %s", input)
t.Errorf("number of matches: %d, want %d", len(findings), len(expected))
for _, e := range expected {
t.Errorf("expected: %s", e)
}
for _, f := range findings {
t.Errorf("got: %d: %s", f.Start.Line, f.Message)
}
return
}
for i := range findings {
msg := fmt.Sprintf(":%d: %s", findings[i].Start.Line, findings[i].Message)
if !strings.Contains(msg, expected[i]) {
t.Errorf("Input: %s", input)
t.Errorf("got: `%s`,\nwant: `%s`", msg, expected[i])
}
}
}
// checkFix makes sure that fixed file contents match the expected output
func checkFix(t *testing.T, categories, input, expected string, scope, fileType build.FileType) {
// If scope doesn't match the file type, no changes are expected
if scope&fileType == 0 {
expected = input
}
file := getFileForTest(input, fileType)
goldenFile := getFileForTest(expected, fileType)
FixWarnings(file, strings.Split(categories, ","), false, testFileReader)
have := build.Format(file)
want := build.Format(goldenFile)
if !bytes.Equal(have, want) {
t.Errorf("fixed a test (type %s) incorrectly:\ninput:\n%s\ndiff (-expected, +ours)\n",
fileType, input)
testutils.Tdiff(t, want, have)
}
}
// checkFix makes sure that the file contents don't change if a fix is not requested
// (i.e. the warning functions have no side effects modifying the AST)
func checkNoFix(t *testing.T, categories, input string, fileType build.FileType) {
file := getFileForTest(input, fileType)
formatted := build.Format(file)
// No fixes expected
FileWarnings(file, strings.Split(categories, ","), nil, ModeWarn, testFileReader)
fixed := build.FormatWithoutRewriting(file)
if !bytes.Equal(formatted, fixed) {
t.Errorf("Modified a file (type %s) while getting warnings:\ninput:\n%s\ndiff (-before, +after)\n",
fileType, input)
testutils.Tdiff(t, formatted, fixed)
}
}
func checkFindings(t *testing.T, category, input string, expected []string, scope build.FileType) {
// The same as checkFindingsAndFix but ensure that fixes don't change the file (except for formatting)
checkFindingsAndFix(t, category, input, input, expected, scope)
}
func checkFindingsAndFix(t *testing.T, categories, input, output string, expected []string, scope build.FileType) {
fileTypes := []build.FileType{
build.TypeDefault,
build.TypeBuild,
build.TypeWorkspace,
build.TypeBzl,
build.TypeModule,
}
for _, fileType := range fileTypes {
compareFindings(t, categories, input, expected, scope, fileType)
checkFix(t, categories, input, output, scope, fileType)
checkFix(t, categories, output, output, scope, fileType)
checkNoFix(t, categories, input, fileType)
}
}
func TestCalculateDifference(t *testing.T) {
tests := []struct {
before string
after string
start int
end int
replacement string
}{
{
before: "asdf",
after: "asxydf",
start: 2,
end: 2,
replacement: "xy",
},
{
before: "asxydf",
after: "asdf",
start: 2,
end: 4,
replacement: "",
},
{
before: "asxydf",
after: "asztdf",
start: 2,
end: 4,
replacement: "zt",
},
{
before: "",
after: "foobar",
start: 0,
end: 0,
replacement: "foobar",
},
{
before: "foobar",
after: "",
start: 0,
end: 6,
replacement: "",
},
{
before: "qwerty",
after: "asdfgh",
start: 0,
end: 6,
replacement: "asdfgh",
},
{
before: "aa",
after: "aaaa",
start: 2,
end: 2,
replacement: "aa",
},
{
before: "aaaa",
after: "aa",
start: 2,
end: 4,
replacement: "",
},
{
before: "abc",
after: "abdbc",
start: 2,
end: 2,
replacement: "db",
},
{
before: "abdbc",
after: "abc",
start: 2,
end: 4,
replacement: "",
},
}
for _, tc := range tests {
before := []byte(tc.before)
after := []byte(tc.after)
start, end, replacement := calculateDifference(&before, &after)
if start != tc.start || end != tc.end || replacement != tc.replacement {
t.Errorf("Wrong difference for %q and %q: want %d, %d, %q, got %d, %d, %q",
tc.before, tc.after, tc.start, tc.end, tc.replacement, start, end, replacement)
}
}
}
func TestSuggestions(t *testing.T) {
// Suggestions are not generated by individual warning functions but by the warnings framework.
contents := `foo()
attr.bar(name = "bar", cfg = "data")
attr.baz("baz", cfg = "data")
`
f, err := build.ParseBzl("file.bzl", []byte(contents))
if err != nil {
t.Fatalf("Parse error: %v", err)
}
findings := FileWarnings(f, []string{"attr-cfg"}, nil, ModeSuggest, testFileReader)
want := []struct {
start int
end int
replacement string
}{
{
start: 28,
end: 42,
replacement: "",
},
{
start: 59,
end: 73,
replacement: "",
},
}
if len(findings) != len(want) {
t.Errorf("Expected %d findings, got %d", len(want), len(findings))
}
for i, f := range findings {
w := want[i]
if f.Replacement == nil {
t.Errorf("No replacement for finding %d", i)
}
r := f.Replacement
if r.Start != w.start || r.End != w.end || r.Content != w.replacement {
t.Errorf("Wrong replacement #%d, want %d, %d, %q, got %d, %d, %q",
i, w.start, w.end, w.replacement, r.Start, r.End, r.Content)
}
}
}
func TestDisabledWarning(t *testing.T) {
contents := `foo()
# buildifier: disable=depset-iteration
for x in depset([1, 2, 3]):
print(x) # buildozer: disable=print
for y in "foobar": # buildozer: disable=string-iteration
# buildifier: disable=no-effect
y
# buildifier: disable=duplicated-name-2
cc_library(
name = "foo", # buildifier: disable=duplicated-name-1
)
# buildifier: disable=skylark-comment
# some comment mentioning skylark
`
f, err := build.ParseBzl("file.bzl", []byte(contents))
if err != nil {
t.Fatalf("Parse error: %v", err)
}
tests := []struct {
start int
end int
category string
}{
{
start: 3,
end: 5,
category: "depset-iteration",
},
{
start: 5,
end: 5,
category: "print",
},
{
start: 7,
end: 7,
category: "string-iteration",
},
{
start: 8,
end: 9,
category: "no-effect",
},
{
start: 13,
end: 13,
category: "duplicated-name-1",
},
{
start: 11,
end: 14,
category: "duplicated-name-2",
},
{
start: 16,
end: 17,
category: "skylark-comment",
},
}
linesCount := strings.Count(contents, "\n")
for _, tc := range tests {
for line := 1; line <= linesCount; line++ {
disabled := DisabledWarning(f, line, tc.category)
shouldBeDisabled := line >= tc.start && line <= tc.end
if disabled != shouldBeDisabled {
t.Errorf("Wrong disabled status for the category %q, want %t, got %t", tc.category, shouldBeDisabled, disabled)
}
}
}
}