| /* |
| 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) |
| } |
| } |
| } |
| } |