github.com/googleapis/api-linter@v1.65.2/cmd/api-linter/integration_test.go (about) 1 // Copyright 2019 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package main 16 17 import ( 18 "errors" 19 "fmt" 20 "os" 21 "path/filepath" 22 "strings" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 ) 27 28 // Each case must be positive when the rule in test 29 // is enabled. It must also contain a "disable-me-here" 30 // comment at the place where you want the rule to be 31 // disabled. 32 var testCases = []struct { 33 testName, rule, proto string 34 }{ 35 { 36 testName: "GetRequestMessage", 37 rule: "core::0131::request-message-name", 38 proto: ` 39 syntax = "proto3"; 40 41 service Library { 42 // disable-me-here 43 rpc GetBook(Book) returns (Book); 44 } 45 46 message Book {} 47 `, 48 }, 49 { 50 testName: "PackageVersion", 51 rule: "core::0215::versioned-packages", 52 proto: ` 53 syntax = "proto3"; 54 55 // disable-me-here 56 package google.test; 57 58 message Test {} 59 `, 60 }, 61 { 62 testName: "FieldNames", 63 rule: "core::0140::lower-snake", 64 proto: ` 65 syntax = "proto3"; 66 import "dummy.proto"; 67 message Test { 68 // disable-me-here 69 string badName = 1; 70 dummy.Dummy dummy = 2; 71 } 72 `, 73 }, 74 } 75 76 func TestRules_EnabledByDefault(t *testing.T) { 77 for _, test := range testCases { 78 t.Run(test.testName, func(t *testing.T) { 79 proto := test.proto 80 result := runLinter(t, proto, "") 81 if !strings.Contains(result, test.rule) { 82 t.Errorf("rule %q should be enabled by default", test.rule) 83 } 84 }) 85 } 86 } 87 88 func TestRules_DisabledByFileComments(t *testing.T) { 89 for _, test := range testCases { 90 t.Run(test.testName, func(t *testing.T) { 91 disableInFile := fmt.Sprintf("// (-- api-linter: %s=disabled --)", test.rule) 92 proto := disableInFile + "\n" + test.proto 93 result := runLinter(t, proto, "") 94 if strings.Contains(result, test.rule) { 95 t.Errorf("rule %q should be disabled by file comments", test.rule) 96 } 97 }) 98 } 99 } 100 101 func TestRules_DisabledByInlineComments(t *testing.T) { 102 testConfigurations := []struct { 103 suffix string 104 appendArgs []string 105 wantDisabled bool 106 }{ 107 { 108 suffix: "WithIgnoreCommentDisablesTrue", 109 appendArgs: []string{"--ignore-comment-disables=true"}, 110 wantDisabled: false, 111 }, 112 { 113 suffix: "WithIgnoreCommentDisablesFalse", 114 appendArgs: []string{"--ignore-comment-disables=false"}, 115 wantDisabled: true, 116 }, 117 { 118 suffix: "WithNoFlags", 119 appendArgs: []string{}, 120 wantDisabled: true, 121 }, 122 } 123 124 for _, test := range testCases { 125 for _, testConfig := range testConfigurations { 126 t.Run(test.testName+testConfig.suffix, func(t *testing.T) { 127 disableInline := fmt.Sprintf("(-- api-linter: %s=disabled --)", test.rule) 128 proto := strings.Replace(test.proto, "disable-me-here", disableInline, -1) 129 _, result := runLinterWithFailureStatus(t, proto, "", testConfig.appendArgs) 130 isDisabled := !strings.Contains(result, test.rule) 131 if isDisabled != testConfig.wantDisabled { 132 t.Errorf("want %q disabled by in-line comments to be %v with flags %v, got %v", test.rule, testConfig.wantDisabled, testConfig.appendArgs, isDisabled) 133 } 134 }) 135 } 136 } 137 } 138 139 func TestRules_DisabledByConfig(t *testing.T) { 140 config := ` 141 [ 142 { 143 "disabled_rules": ["replace-me-here"] 144 } 145 ] 146 ` 147 148 for _, test := range testCases { 149 t.Run(test.testName, func(t *testing.T) { 150 c := strings.Replace(config, "replace-me-here", test.rule, -1) 151 result := runLinter(t, test.proto, c) 152 if strings.Contains(result, test.rule) { 153 t.Errorf("rule %q should be disabled by the user config: %q", test.rule, c) 154 } 155 }) 156 } 157 } 158 159 func TestBuildErrors(t *testing.T) { 160 expected := []string{ 161 "internal/testdata/build_errors.proto:8:1:", 162 "internal/testdata/build_errors.proto:13:1:", 163 } 164 err := runCLI([]string{"internal/testdata/build_errors.proto"}) 165 if err == nil { 166 t.Fatal("expected build error for build_errors.proto") 167 } 168 actual := err.Error() 169 actualLines := strings.Split(strings.TrimSpace(actual), "\n") 170 for idx, line := range actualLines { 171 if idx := strings.IndexByte(line, ' '); idx > -1 { 172 line = line[:idx] 173 } 174 actualLines[idx] = line 175 } 176 if diff := cmp.Diff(expected, actualLines); diff != "" { 177 t.Fatalf("unexpected errors: diff (-want +got):\n%s", diff) 178 } 179 } 180 181 func TestExitStatusForLintFailure(t *testing.T) { 182 type testCase struct{ testName, rule, proto string } 183 failCase := testCase{ 184 testName: "GetRequestMessage", 185 rule: "core::0131::request-message-name", 186 proto: ` 187 syntax = "proto3"; 188 189 service Library { 190 // disable-me-here 191 rpc GetBook(Book) returns (Book); 192 } 193 194 message Book {} 195 `, 196 } 197 // checks lint failure = true when lint problems found 198 t.Run(failCase.testName+"ReturnsFailure", func(t *testing.T) { 199 lintFailureStatus, result := runLinterWithFailureStatus(t, failCase.proto, "", []string{"--set-exit-status"}) 200 if lintFailureStatus == false { 201 t.Log(result) 202 t.Fatalf("Expected: %v Actual: %v", true, lintFailureStatus) 203 } 204 }) 205 206 // checks lint failure = false when no problems found 207 t.Run(failCase.testName+"ReturnsNoFailure", func(t *testing.T) { 208 disableAll := `[ { "disabled_rules": [ "all" ] } ]` 209 lintFailureStatus, result := runLinterWithFailureStatus(t, failCase.proto, disableAll, []string{"--set-exit-status"}) 210 if lintFailureStatus { 211 t.Log(result) 212 } 213 if lintFailureStatus == true { 214 t.Fatalf("Expected: %v Actual: %v", false, lintFailureStatus) 215 } 216 }) 217 218 // checks lint failure = false when lint problems found but --set-exit-status not set 219 for _, test := range testCases { 220 t.Run(test.testName, func(t *testing.T) { 221 proto := test.proto 222 lintFailureStatus, result := runLinterWithFailureStatus(t, proto, "", []string{}) 223 expected := result == "" 224 if lintFailureStatus != expected { 225 t.Fatalf("Expected: %v Actual: %v", expected, lintFailureStatus) 226 } 227 }) 228 } 229 } 230 231 func runLinter(t *testing.T, protoContent, configContent string) string { 232 _, result := runLinterWithFailureStatus(t, protoContent, configContent, []string{}) 233 return result 234 } 235 236 func runLinterWithFailureStatus(t *testing.T, protoContent, configContent string, appendArgs []string) (bool, string) { 237 tempDir, err := os.MkdirTemp("", "test") 238 if err != nil { 239 t.Fatal(err) 240 } 241 defer os.RemoveAll(tempDir) 242 243 // Prepare command line flags. 244 args := []string{} 245 // Add a flag for the linter config file if the provided 246 // config content is not empty. 247 if configContent != "" { 248 configFileName := "test_config.json" 249 configFilePath := filepath.Join(tempDir, configFileName) 250 if err := writeFile(configFilePath, configContent); err != nil { 251 t.Fatal(err) 252 } 253 args = append(args, fmt.Sprintf("--config=%s", configFilePath)) 254 } 255 // Add a flag for the output path. 256 outPath := filepath.Join(tempDir, "test.out") 257 args = append(args, fmt.Sprintf("-o=%s", outPath)) 258 // Add the temp dir to the proto paths. 259 args = append(args, fmt.Sprintf("-I=%s", tempDir)) 260 // Add a flag for the file descriptor set. 261 args = append(args, "--descriptor-set-in=internal/testdata/dummy.protoset") 262 // Write the proto file. 263 protoFileName := "test.proto" 264 protoFilePath := filepath.Join(tempDir, protoFileName) 265 if err := writeFile(protoFilePath, protoContent); err != nil { 266 t.Fatal(err) 267 } 268 args = append(args, protoFileName) 269 args = append(args, appendArgs...) 270 271 lintErr := runCLI(args) 272 if lintErr != nil && !errors.Is(lintErr, ExitForLintFailure) { 273 t.Fatal(lintErr) 274 } 275 276 out, err := os.ReadFile(outPath) 277 if err != nil { 278 t.Fatal(err) 279 } 280 return errors.Is(lintErr, ExitForLintFailure), string(out) 281 } 282 283 func writeFile(path, content string) error { 284 if path == "" { 285 return nil 286 } 287 err := os.MkdirAll(filepath.Dir(path), os.ModePerm) 288 if err != nil { 289 return err 290 } 291 return os.WriteFile(path, []byte(content), 0o644) 292 }