github.com/chenfeining/golangci-lint@v1.0.2-0.20230730162517-14c6c67868df/test/testshared/runner.go (about)

     1  package testshared
     2  
     3  import (
     4  	"os"
     5  	"os/exec"
     6  	"path/filepath"
     7  	"strings"
     8  	"sync"
     9  	"syscall"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/chenfeining/golangci-lint/pkg/exitcodes"
    17  	"github.com/chenfeining/golangci-lint/pkg/fsutils"
    18  	"github.com/chenfeining/golangci-lint/pkg/logutils"
    19  )
    20  
    21  const (
    22  	// value: "1"
    23  	envKeepTempFiles = "GL_KEEP_TEMP_FILES"
    24  	// value: "true"
    25  	envGolangciLintInstalled = "GOLANGCI_LINT_INSTALLED"
    26  )
    27  
    28  type RunnerBuilder struct {
    29  	tb  testing.TB
    30  	log logutils.Log
    31  
    32  	binPath string
    33  	command string
    34  	env     []string
    35  
    36  	configPath           string
    37  	noConfig             bool
    38  	allowParallelRunners bool
    39  	args                 []string
    40  	target               string
    41  }
    42  
    43  func NewRunnerBuilder(tb testing.TB) *RunnerBuilder {
    44  	tb.Helper()
    45  
    46  	log := logutils.NewStderrLog(logutils.DebugKeyTest)
    47  	log.SetLevel(logutils.LogLevelInfo)
    48  
    49  	return &RunnerBuilder{
    50  		tb:                   tb,
    51  		log:                  log,
    52  		binPath:              defaultBinaryName(),
    53  		command:              "run",
    54  		allowParallelRunners: true,
    55  	}
    56  }
    57  
    58  func (b *RunnerBuilder) WithBinPath(binPath string) *RunnerBuilder {
    59  	b.binPath = binPath
    60  
    61  	return b
    62  }
    63  
    64  func (b *RunnerBuilder) WithCommand(command string) *RunnerBuilder {
    65  	b.command = command
    66  
    67  	return b
    68  }
    69  
    70  func (b *RunnerBuilder) WithNoConfig() *RunnerBuilder {
    71  	b.noConfig = true
    72  
    73  	return b
    74  }
    75  
    76  func (b *RunnerBuilder) WithConfigFile(cfgPath string) *RunnerBuilder {
    77  	if cfgPath != "" {
    78  		b.configPath = filepath.FromSlash(cfgPath)
    79  	}
    80  
    81  	b.noConfig = cfgPath == ""
    82  
    83  	return b
    84  }
    85  
    86  func (b *RunnerBuilder) WithConfig(cfg string) *RunnerBuilder {
    87  	b.tb.Helper()
    88  
    89  	content := strings.ReplaceAll(strings.TrimSpace(cfg), "\t", " ")
    90  
    91  	if content == "" {
    92  		return b.WithNoConfig()
    93  	}
    94  
    95  	cfgFile, err := os.CreateTemp("", "golangci_lint_test*.yml")
    96  	require.NoError(b.tb, err)
    97  
    98  	cfgPath := cfgFile.Name()
    99  	b.tb.Cleanup(func() {
   100  		if os.Getenv(envKeepTempFiles) != "1" {
   101  			_ = os.Remove(cfgPath)
   102  		}
   103  	})
   104  
   105  	_, err = cfgFile.WriteString(content)
   106  	require.NoError(b.tb, err)
   107  
   108  	return b.WithConfigFile(cfgPath)
   109  }
   110  
   111  func (b *RunnerBuilder) WithRunContext(rc *RunContext) *RunnerBuilder {
   112  	if rc == nil {
   113  		return b
   114  	}
   115  
   116  	dir, err := os.Getwd()
   117  	require.NoError(b.tb, err)
   118  
   119  	configPath := filepath.FromSlash(rc.ConfigPath)
   120  
   121  	base := filepath.Base(dir)
   122  	if strings.HasPrefix(configPath, base) {
   123  		configPath = strings.TrimPrefix(configPath, base+string(filepath.Separator))
   124  	}
   125  
   126  	return b.WithConfigFile(configPath).WithArgs(rc.Args...)
   127  }
   128  
   129  func (b *RunnerBuilder) WithDirectives(sourcePath string) *RunnerBuilder {
   130  	b.tb.Helper()
   131  
   132  	return b.WithRunContext(ParseTestDirectives(b.tb, sourcePath))
   133  }
   134  
   135  func (b *RunnerBuilder) WithEnviron(environ ...string) *RunnerBuilder {
   136  	b.env = environ
   137  
   138  	return b
   139  }
   140  
   141  func (b *RunnerBuilder) WithNoParallelRunners() *RunnerBuilder {
   142  	b.allowParallelRunners = false
   143  
   144  	return b
   145  }
   146  
   147  func (b *RunnerBuilder) WithArgs(args ...string) *RunnerBuilder {
   148  	b.args = append(b.args, args...)
   149  
   150  	return b
   151  }
   152  
   153  func (b *RunnerBuilder) WithTargetPath(targets ...string) *RunnerBuilder {
   154  	b.target = filepath.Join(targets...)
   155  
   156  	return b
   157  }
   158  
   159  func (b *RunnerBuilder) Runner() *Runner {
   160  	b.tb.Helper()
   161  
   162  	if b.noConfig && b.configPath != "" {
   163  		b.tb.Fatal("--no-config and -c cannot be used at the same time")
   164  	}
   165  
   166  	arguments := []string{
   167  		"--internal-cmd-test",
   168  	}
   169  
   170  	if b.allowParallelRunners {
   171  		arguments = append(arguments, "--allow-parallel-runners")
   172  	}
   173  
   174  	if b.noConfig {
   175  		arguments = append(arguments, "--no-config")
   176  	}
   177  
   178  	if b.configPath != "" {
   179  		arguments = append(arguments, "-c", b.configPath)
   180  	}
   181  
   182  	if len(b.args) != 0 {
   183  		arguments = append(arguments, b.args...)
   184  	}
   185  
   186  	if b.target != "" {
   187  		arguments = append(arguments, b.target)
   188  	}
   189  
   190  	return &Runner{
   191  		binPath: b.binPath,
   192  		log:     b.log,
   193  		tb:      b.tb,
   194  		env:     b.env,
   195  		command: b.command,
   196  		args:    arguments,
   197  	}
   198  }
   199  
   200  type Runner struct {
   201  	log logutils.Log
   202  	tb  testing.TB
   203  
   204  	binPath string
   205  	env     []string
   206  	command string
   207  	args    []string
   208  
   209  	installOnce sync.Once
   210  }
   211  
   212  func (r *Runner) Install() *Runner {
   213  	r.tb.Helper()
   214  
   215  	r.installOnce.Do(func() {
   216  		InstallGolangciLint(r.tb)
   217  	})
   218  
   219  	return r
   220  }
   221  
   222  func (r *Runner) Run() *RunnerResult {
   223  	r.tb.Helper()
   224  
   225  	runArgs := append([]string{r.command}, r.args...)
   226  
   227  	defer func(startedAt time.Time) {
   228  		r.log.Infof("ran [%s %s] in %s", r.binPath, strings.Join(runArgs, " "), time.Since(startedAt))
   229  	}(time.Now())
   230  
   231  	cmd := r.Command()
   232  
   233  	out, err := cmd.CombinedOutput()
   234  	if err != nil {
   235  		if exitError, ok := err.(*exec.ExitError); ok {
   236  			if len(exitError.Stderr) != 0 {
   237  				r.log.Infof("stderr: %s", exitError.Stderr)
   238  			}
   239  
   240  			ws := exitError.Sys().(syscall.WaitStatus)
   241  
   242  			return &RunnerResult{
   243  				tb:       r.tb,
   244  				output:   string(out),
   245  				exitCode: ws.ExitStatus(),
   246  			}
   247  		}
   248  
   249  		r.tb.Errorf("can't get error code from %s", err)
   250  
   251  		return nil
   252  	}
   253  
   254  	// success, exitCode should be 0 if go is ok
   255  	ws := cmd.ProcessState.Sys().(syscall.WaitStatus)
   256  
   257  	return &RunnerResult{
   258  		tb:       r.tb,
   259  		output:   string(out),
   260  		exitCode: ws.ExitStatus(),
   261  	}
   262  }
   263  
   264  func (r *Runner) Command() *exec.Cmd {
   265  	r.tb.Helper()
   266  
   267  	runArgs := append([]string{r.command}, r.args...)
   268  
   269  	//nolint:gosec
   270  	cmd := exec.Command(r.binPath, runArgs...)
   271  	cmd.Env = append(os.Environ(), r.env...)
   272  
   273  	return cmd
   274  }
   275  
   276  type RunnerResult struct {
   277  	tb testing.TB
   278  
   279  	output   string
   280  	exitCode int
   281  }
   282  
   283  func (r *RunnerResult) ExpectNoIssues() {
   284  	r.tb.Helper()
   285  
   286  	assert.Equal(r.tb, "", r.output, "exit code is %d", r.exitCode)
   287  	assert.Equal(r.tb, exitcodes.Success, r.exitCode, "output is %s", r.output)
   288  }
   289  
   290  func (r *RunnerResult) ExpectExitCode(possibleCodes ...int) *RunnerResult {
   291  	r.tb.Helper()
   292  
   293  	for _, pc := range possibleCodes {
   294  		if pc == r.exitCode {
   295  			return r
   296  		}
   297  	}
   298  
   299  	assert.Fail(r.tb, "invalid exit code", "exit code (%d) must be one of %v: %s", r.exitCode, possibleCodes, r.output)
   300  	return r
   301  }
   302  
   303  // ExpectOutputRegexp can be called with either a string or compiled regexp
   304  func (r *RunnerResult) ExpectOutputRegexp(s string) *RunnerResult {
   305  	r.tb.Helper()
   306  
   307  	assert.Regexp(r.tb, fsutils.NormalizePathInRegex(s), r.output, "exit code is %d", r.exitCode)
   308  	return r
   309  }
   310  
   311  func (r *RunnerResult) ExpectOutputContains(s ...string) *RunnerResult {
   312  	r.tb.Helper()
   313  
   314  	for _, expected := range s {
   315  		assert.Contains(r.tb, r.output, normalizeFilePath(expected), "exit code is %d", r.exitCode)
   316  	}
   317  
   318  	return r
   319  }
   320  
   321  func (r *RunnerResult) ExpectOutputNotContains(s string) *RunnerResult {
   322  	r.tb.Helper()
   323  
   324  	assert.NotContains(r.tb, r.output, s, "exit code is %d", r.exitCode)
   325  	return r
   326  }
   327  
   328  func (r *RunnerResult) ExpectOutputEq(s string) *RunnerResult {
   329  	r.tb.Helper()
   330  
   331  	assert.Equal(r.tb, normalizeFilePath(s), r.output, "exit code is %d", r.exitCode)
   332  	return r
   333  }
   334  
   335  func (r *RunnerResult) ExpectHasIssue(issueText string) *RunnerResult {
   336  	r.tb.Helper()
   337  
   338  	return r.ExpectExitCode(exitcodes.IssuesFound).ExpectOutputContains(issueText)
   339  }
   340  
   341  func InstallGolangciLint(tb testing.TB) string {
   342  	tb.Helper()
   343  
   344  	if os.Getenv(envGolangciLintInstalled) != "true" {
   345  		cmd := exec.Command("make", "-C", "..", "build")
   346  
   347  		output, err := cmd.CombinedOutput()
   348  		if err != nil {
   349  			tb.Log(string(output))
   350  		}
   351  
   352  		assert.NoError(tb, err, "Can't go install golangci-lint %s", string(output))
   353  	}
   354  
   355  	abs, err := filepath.Abs(defaultBinaryName())
   356  	require.NoError(tb, err)
   357  
   358  	return abs
   359  }