github.com/yuqengo/golangci-lint@v0.0.2/test/linters_test.go (about) 1 package test 2 3 import ( 4 "bufio" 5 "fmt" 6 "go/build/constraint" 7 "os" 8 "os/exec" 9 "path" 10 "path/filepath" 11 "runtime" 12 "strings" 13 "testing" 14 15 hcversion "github.com/hashicorp/go-version" 16 "github.com/stretchr/testify/require" 17 "gopkg.in/yaml.v3" 18 19 "github.com/golangci/golangci-lint/pkg/exitcodes" 20 "github.com/golangci/golangci-lint/test/testshared" 21 ) 22 23 func runGoErrchk(c *exec.Cmd, defaultExpectedLinter string, files []string, t *testing.T) { 24 output, err := c.CombinedOutput() 25 // The returned error will be nil if the test file does not have any issues 26 // and thus the linter exits with exit code 0. So perform the additional 27 // assertions only if the error is non-nil. 28 if err != nil { 29 var exitErr *exec.ExitError 30 require.ErrorAs(t, err, &exitErr) 31 require.Equal(t, exitcodes.IssuesFound, exitErr.ExitCode(), "Unexpected exit code: %s", string(output)) 32 } 33 34 fullshort := make([]string, 0, len(files)*2) 35 for _, f := range files { 36 fullshort = append(fullshort, f, filepath.Base(f)) 37 } 38 39 err = errorCheck(string(output), false, defaultExpectedLinter, fullshort...) 40 require.NoError(t, err) 41 } 42 43 func testSourcesFromDir(t *testing.T, dir string) { 44 t.Log(filepath.Join(dir, "*.go")) 45 46 findSources := func(pathPatterns ...string) []string { 47 sources, err := filepath.Glob(filepath.Join(pathPatterns...)) 48 require.NoError(t, err) 49 require.NotEmpty(t, sources) 50 return sources 51 } 52 sources := findSources(dir, "*.go") 53 54 testshared.NewLintRunner(t).Install() 55 56 for _, s := range sources { 57 s := s 58 t.Run(filepath.Base(s), func(subTest *testing.T) { 59 subTest.Parallel() 60 testOneSource(subTest, s) 61 }) 62 } 63 } 64 65 func TestSourcesFromTestdataWithIssuesDir(t *testing.T) { 66 testSourcesFromDir(t, testdataDir) 67 } 68 69 func TestTypecheck(t *testing.T) { 70 testSourcesFromDir(t, filepath.Join(testdataDir, "notcompiles")) 71 } 72 73 func TestGoimportsLocal(t *testing.T) { 74 sourcePath := filepath.Join(testdataDir, "goimports", "goimports.go") 75 args := []string{ 76 "--disable-all", "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number", 77 sourcePath, 78 } 79 80 rc := extractRunContextFromComments(t, sourcePath) 81 require.NotNil(t, rc) 82 83 args = append(args, rc.args...) 84 85 cfg, err := yaml.Marshal(rc.config) 86 require.NoError(t, err) 87 88 testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...). 89 ExpectHasIssue("testdata/goimports/goimports.go:8: File is not `goimports`-ed") 90 } 91 92 func TestGciLocal(t *testing.T) { 93 sourcePath := filepath.Join(testdataDir, "gci", "gci.go") 94 args := []string{ 95 "--disable-all", "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number", 96 sourcePath, 97 } 98 99 rc := extractRunContextFromComments(t, sourcePath) 100 require.NotNil(t, rc) 101 102 args = append(args, rc.args...) 103 104 cfg, err := os.ReadFile(rc.configPath) 105 require.NoError(t, err) 106 107 testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...). 108 ExpectHasIssue("testdata/gci/gci.go:8: File is not `gci`-ed") 109 } 110 111 func TestMultipleOutputs(t *testing.T) { 112 sourcePath := filepath.Join(testdataDir, "gci", "gci.go") 113 args := []string{ 114 "--disable-all", "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number,json:stdout", 115 sourcePath, 116 } 117 118 rc := extractRunContextFromComments(t, sourcePath) 119 require.NotNil(t, rc) 120 121 args = append(args, rc.args...) 122 123 cfg, err := os.ReadFile(rc.configPath) 124 require.NoError(t, err) 125 126 testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...). 127 ExpectHasIssue("testdata/gci/gci.go:8: File is not `gci`-ed"). 128 ExpectOutputContains(`"Issues":[`) 129 } 130 131 func TestStderrOutput(t *testing.T) { 132 sourcePath := filepath.Join(testdataDir, "gci", "gci.go") 133 args := []string{ 134 "--disable-all", "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number,json:stderr", 135 sourcePath, 136 } 137 138 rc := extractRunContextFromComments(t, sourcePath) 139 require.NotNil(t, rc) 140 141 args = append(args, rc.args...) 142 143 cfg, err := os.ReadFile(rc.configPath) 144 require.NoError(t, err) 145 146 testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...). 147 ExpectHasIssue("testdata/gci/gci.go:8: File is not `gci`-ed"). 148 ExpectOutputContains(`"Issues":[`) 149 } 150 151 func TestFileOutput(t *testing.T) { 152 resultPath := path.Join(t.TempDir(), "golangci_lint_test_result") 153 154 sourcePath := filepath.Join(testdataDir, "gci", "gci.go") 155 args := []string{ 156 "--disable-all", "--print-issued-lines=false", "--print-linter-name=false", 157 fmt.Sprintf("--out-format=json:%s,line-number", resultPath), 158 sourcePath, 159 } 160 161 rc := extractRunContextFromComments(t, sourcePath) 162 require.NotNil(t, rc) 163 164 args = append(args, rc.args...) 165 166 cfg, err := os.ReadFile(rc.configPath) 167 require.NoError(t, err) 168 169 testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...). 170 ExpectHasIssue("testdata/gci/gci.go:8: File is not `gci`-ed"). 171 ExpectOutputNotContains(`"Issues":[`) 172 173 b, err := os.ReadFile(resultPath) 174 require.NoError(t, err) 175 require.Contains(t, string(b), `"Issues":[`) 176 } 177 178 func saveConfig(t *testing.T, cfg map[string]interface{}) (cfgPath string, finishFunc func()) { 179 f, err := os.CreateTemp("", "golangci_lint_test") 180 require.NoError(t, err) 181 182 cfgPath = f.Name() + ".yml" 183 err = os.Rename(f.Name(), cfgPath) 184 require.NoError(t, err) 185 186 err = yaml.NewEncoder(f).Encode(cfg) 187 require.NoError(t, err) 188 189 return cfgPath, func() { 190 require.NoError(t, f.Close()) 191 if os.Getenv("GL_KEEP_TEMP_FILES") != "1" { 192 require.NoError(t, os.Remove(cfgPath)) 193 } 194 } 195 } 196 197 func testOneSource(t *testing.T, sourcePath string) { 198 args := []string{ 199 "run", 200 "--go=1.17", // TODO(ldez): we force to use an old version of Go for the CI and the tests. 201 "--allow-parallel-runners", 202 "--disable-all", 203 "--print-issued-lines=false", 204 "--out-format=line-number", 205 "--max-same-issues=100", 206 } 207 208 rc := extractRunContextFromComments(t, sourcePath) 209 if rc == nil { 210 t.Skipf("Skipped: %s", sourcePath) 211 } 212 213 var cfgPath string 214 if rc.config != nil { 215 p, finish := saveConfig(t, rc.config) 216 defer finish() 217 cfgPath = p 218 } else if rc.configPath != "" { 219 cfgPath = rc.configPath 220 } 221 222 for _, addArg := range []string{"", "-Etypecheck"} { 223 caseArgs := append([]string{}, args...) 224 caseArgs = append(caseArgs, rc.args...) 225 if addArg != "" { 226 caseArgs = append(caseArgs, addArg) 227 } 228 if cfgPath == "" { 229 caseArgs = append(caseArgs, "--no-config") 230 } else { 231 caseArgs = append(caseArgs, "-c", cfgPath) 232 } 233 234 caseArgs = append(caseArgs, sourcePath) 235 236 cmd := exec.Command(binName, caseArgs...) 237 t.Log(caseArgs) 238 runGoErrchk(cmd, rc.expectedLinter, []string{sourcePath}, t) 239 } 240 } 241 242 type runContext struct { 243 args []string 244 config map[string]interface{} 245 configPath string 246 expectedLinter string 247 } 248 249 func buildConfigFromShortRepr(t *testing.T, repr string, config map[string]interface{}) { 250 kv := strings.Split(repr, "=") 251 require.Len(t, kv, 2, "repr: %s", repr) 252 253 keyParts := strings.Split(kv[0], ".") 254 require.True(t, len(keyParts) >= 2, len(keyParts)) 255 256 lastObj := config 257 for _, k := range keyParts[:len(keyParts)-1] { 258 var v map[string]interface{} 259 if lastObj[k] == nil { 260 v = map[string]interface{}{} 261 } else { 262 v = lastObj[k].(map[string]interface{}) 263 } 264 265 lastObj[k] = v 266 lastObj = v 267 } 268 269 lastObj[keyParts[len(keyParts)-1]] = kv[1] 270 } 271 272 func skipMultilineComment(scanner *bufio.Scanner) { 273 for line := scanner.Text(); !strings.Contains(line, "*/") && scanner.Scan(); { 274 line = scanner.Text() 275 } 276 } 277 278 //nolint:gocyclo,funlen 279 func extractRunContextFromComments(t *testing.T, sourcePath string) *runContext { 280 f, err := os.Open(sourcePath) 281 require.NoError(t, err) 282 defer f.Close() 283 284 rc := &runContext{} 285 286 scanner := bufio.NewScanner(f) 287 for scanner.Scan() { 288 line := scanner.Text() 289 if strings.HasPrefix(line, "/*") { 290 skipMultilineComment(scanner) 291 continue 292 } 293 if strings.TrimSpace(line) == "" { 294 continue 295 } 296 if !strings.HasPrefix(line, "//") { 297 break 298 } 299 300 if strings.HasPrefix(line, "//go:build") || strings.HasPrefix(line, "// +build") { 301 parse, err := constraint.Parse(line) 302 require.NoError(t, err) 303 304 if !parse.Eval(buildTagGoVersion) { 305 return nil 306 } 307 308 continue 309 } 310 311 if !strings.HasPrefix(line, "//golangcitest:") { 312 require.Failf(t, "invalid prefix of comment line %s", line) 313 } 314 315 before, after, found := strings.Cut(line, " ") 316 require.Truef(t, found, "invalid prefix of comment line %s", line) 317 318 after = strings.TrimSpace(after) 319 320 switch before { 321 case "//golangcitest:args": 322 require.Nil(t, rc.args) 323 require.NotEmpty(t, after) 324 rc.args = strings.Split(after, " ") 325 continue 326 327 case "//golangcitest:config": 328 require.NotEmpty(t, after) 329 if rc.config == nil { 330 rc.config = map[string]interface{}{} 331 } 332 buildConfigFromShortRepr(t, after, rc.config) 333 continue 334 335 case "//golangcitest:config_path": 336 require.NotEmpty(t, after) 337 rc.configPath = after 338 continue 339 340 case "//golangcitest:expected_linter": 341 require.NotEmpty(t, after) 342 rc.expectedLinter = after 343 continue 344 345 default: 346 require.Failf(t, "invalid prefix of comment line %s", line) 347 } 348 } 349 350 // guess the expected linter if none is specified 351 if rc.expectedLinter == "" { 352 for _, arg := range rc.args { 353 if strings.HasPrefix(arg, "-E") && !strings.Contains(arg, ",") { 354 require.Empty(t, rc.expectedLinter, "could not infer expected linter for errors because multiple linters are enabled. Please use the `//golangcitest:expected_linter ` directive in your test to indicate the linter-under-test.") //nolint:lll 355 rc.expectedLinter = arg[2:] 356 } 357 } 358 } 359 360 return rc 361 } 362 363 func buildTagGoVersion(tag string) bool { 364 vRuntime, err := hcversion.NewVersion(strings.TrimPrefix(runtime.Version(), "go")) 365 if err != nil { 366 return false 367 } 368 369 vTag, err := hcversion.NewVersion(strings.TrimPrefix(tag, "go")) 370 if err != nil { 371 return false 372 } 373 374 return vRuntime.GreaterThanOrEqual(vTag) 375 } 376 377 func TestExtractRunContextFromComments(t *testing.T) { 378 rc := extractRunContextFromComments(t, filepath.Join(testdataDir, "goimports", "goimports.go")) 379 require.NotNil(t, rc) 380 require.Equal(t, []string{"-Egoimports"}, rc.args) 381 } 382 383 func TestTparallel(t *testing.T) { 384 t.Run("should fail on missing top-level Parallel()", func(t *testing.T) { 385 sourcePath := filepath.Join(testdataDir, "tparallel", "missing_toplevel_test.go") 386 args := []string{ 387 "--disable-all", "--enable", "tparallel", 388 "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number", 389 sourcePath, 390 } 391 392 rc := extractRunContextFromComments(t, sourcePath) 393 require.NotNil(t, rc) 394 395 args = append(args, rc.args...) 396 397 cfg, err := yaml.Marshal(rc.config) 398 require.NoError(t, err) 399 400 testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...). 401 ExpectHasIssue( 402 "testdata/tparallel/missing_toplevel_test.go:7:6: TestTopLevel should call t.Parallel on the top level as well as its subtests\n", 403 ) 404 }) 405 406 t.Run("should fail on missing subtest Parallel()", func(t *testing.T) { 407 sourcePath := filepath.Join(testdataDir, "tparallel", "missing_subtest_test.go") 408 args := []string{ 409 "--disable-all", "--enable", "tparallel", 410 "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number", 411 sourcePath, 412 } 413 414 rc := extractRunContextFromComments(t, sourcePath) 415 require.NotNil(t, rc) 416 417 args = append(args, rc.args...) 418 419 cfg, err := yaml.Marshal(rc.config) 420 require.NoError(t, err) 421 422 testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...). 423 ExpectHasIssue( 424 "testdata/tparallel/missing_subtest_test.go:7:6: TestSubtests's subtests should call t.Parallel\n", 425 ) 426 }) 427 428 t.Run("should pass on parallel test with no subtests", func(t *testing.T) { 429 sourcePath := filepath.Join(testdataDir, "tparallel", "happy_path_test.go") 430 args := []string{ 431 "--disable-all", "--enable", "tparallel", 432 "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number", 433 sourcePath, 434 } 435 436 rc := extractRunContextFromComments(t, sourcePath) 437 require.NotNil(t, rc) 438 439 args = append(args, rc.args...) 440 441 cfg, err := yaml.Marshal(rc.config) 442 require.NoError(t, err) 443 444 testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...).ExpectNoIssues() 445 }) 446 }