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 }