test(lint): task lint must exit non-zero on a finding (#41), red
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
package lintgate
|
||||||
|
|
||||||
|
// Observes the exit status of `task lint` (the lint step of `task check`), not the lint
|
||||||
|
// rules themselves. It runs the repo's real Taskfile in a scratch module so a change to the
|
||||||
|
// lint command that stops it failing on findings (#41) turns this red.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const scratchWithFinding = `package main
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func main() { os.Remove("x") }
|
||||||
|
`
|
||||||
|
|
||||||
|
func scratchModule(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
taskfile, err := os.ReadFile(filepath.Join("..", "..", "Taskfile.yml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read Taskfile.yml: %v", err)
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
for name, body := range map[string]string{
|
||||||
|
"Taskfile.yml": string(taskfile),
|
||||||
|
"go.mod": "module scratch\n\ngo 1.21\n",
|
||||||
|
"main.go": scratchWithFinding,
|
||||||
|
} {
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
|
||||||
|
t.Fatalf("write %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// runLint runs `task lint` in dir with the given PATH and returns its combined output and exit code.
|
||||||
|
func runLint(t *testing.T, dir, path string) (string, int) {
|
||||||
|
t.Helper()
|
||||||
|
taskBin, err := exec.LookPath("task")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("task not installed")
|
||||||
|
}
|
||||||
|
cmd := exec.Command(taskBin, "lint")
|
||||||
|
cmd.Dir = dir
|
||||||
|
cmd.Env = append(os.Environ(), "PATH="+path)
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout, cmd.Stderr = &out, &out
|
||||||
|
err = cmd.Run()
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return out.String(), 0
|
||||||
|
case errors.As(err, &exitErr):
|
||||||
|
return out.String(), exitErr.ExitCode()
|
||||||
|
default:
|
||||||
|
t.Fatalf("run task lint: %v", err)
|
||||||
|
return "", -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskLint_ExitsNonZeroWhenGolangciLintReportsAFinding(t *testing.T) {
|
||||||
|
if _, err := exec.LookPath("golangci-lint"); err != nil {
|
||||||
|
t.Skip("golangci-lint not installed")
|
||||||
|
}
|
||||||
|
out, code := runLint(t, scratchModule(t), os.Getenv("PATH"))
|
||||||
|
if code == 0 {
|
||||||
|
t.Fatalf("task lint exited 0 on a module with an unchecked error; output:\n%s", out)
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "not installed") {
|
||||||
|
t.Fatalf("task lint blamed a missing binary for a lint failure; output:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskLint_SkipsWithANoticeWhenGolangciLintIsAbsent(t *testing.T) {
|
||||||
|
taskBin, err := exec.LookPath("task")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("task not installed")
|
||||||
|
}
|
||||||
|
bin := t.TempDir() // a PATH holding task and nothing else
|
||||||
|
if err := os.Symlink(taskBin, filepath.Join(bin, "task")); err != nil {
|
||||||
|
t.Fatalf("symlink task: %v", err)
|
||||||
|
}
|
||||||
|
out, code := runLint(t, scratchModule(t), bin)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("task lint exited %d without golangci-lint on PATH; output:\n%s", code, out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "not installed") {
|
||||||
|
t.Fatalf("expected the skip notice; output:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user