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  }