diff --git a/test/lintgate/lintgate_test.go b/test/lintgate/lintgate_test.go new file mode 100644 index 0000000..c924abb --- /dev/null +++ b/test/lintgate/lintgate_test.go @@ -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) + } +}