ceremonyclient/pebble/internal/lint/lint_test.go

302 lines
7.7 KiB
Go
Raw Permalink Normal View History

2024-01-03 07:31:42 +00:00
// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package lint
import (
"bytes"
"fmt"
"go/build"
"os/exec"
"regexp"
"runtime"
"strings"
"testing"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/ghemawat/stream"
"github.com/stretchr/testify/require"
)
const (
cmdGo = "go"
golint = "golang.org/x/lint/golint@6edffad5e6160f5949cdefc81710b2706fbcd4f6"
staticcheck = "honnef.co/go/tools/cmd/staticcheck@2023.1"
crlfmt = "github.com/cockroachdb/crlfmt@44a36ec7"
)
func dirCmd(t *testing.T, dir string, name string, args ...string) stream.Filter {
cmd := exec.Command(name, args...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
switch err.(type) {
case nil:
case *exec.ExitError:
// Non-zero exit is expected.
default:
require.NoError(t, err)
}
return stream.ReadLines(bytes.NewReader(out))
}
func ignoreGoMod() stream.Filter {
return stream.GrepNot(`^go: (finding|extracting|downloading)`)
}
func TestLint(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("lint checks skipped on Windows")
}
if invariants.RaceEnabled {
// We are not interested in race-testing the linters themselves.
t.Skip("lint checks skipped on race builds")
}
const root = "github.com/cockroachdb/pebble"
pkg, err := build.Import(root, "../..", 0)
require.NoError(t, err)
var pkgs []string
if err := stream.ForEach(
stream.Sequence(
dirCmd(t, pkg.Dir, "go", "list", "./..."),
ignoreGoMod(),
), func(s string) {
pkgs = append(pkgs, s)
}); err != nil {
require.NoError(t, err)
}
t.Run("TestGolint", func(t *testing.T) {
t.Parallel()
args := []string{"run", golint}
args = append(args, pkgs...)
// This is overkill right now, but provides a structure for filtering out
// lint errors we don't care about.
if err := stream.ForEach(
stream.Sequence(
dirCmd(t, pkg.Dir, cmdGo, args...),
stream.GrepNot("go: downloading"),
), func(s string) {
t.Errorf("\n%s", s)
}); err != nil {
t.Error(err)
}
})
t.Run("TestStaticcheck", func(t *testing.T) {
t.Parallel()
args := []string{"run", staticcheck}
args = append(args, pkgs...)
if err := stream.ForEach(
stream.Sequence(
dirCmd(t, pkg.Dir, cmdGo, args...),
stream.GrepNot("go: downloading"),
), func(s string) {
t.Errorf("\n%s", s)
}); err != nil {
t.Error(err)
}
})
t.Run("TestGoVet", func(t *testing.T) {
t.Parallel()
if err := stream.ForEach(
stream.Sequence(
dirCmd(t, pkg.Dir, "go", "vet", "-all", "./..."),
stream.GrepNot(`^#`), // ignore comment lines
ignoreGoMod(),
), func(s string) {
t.Errorf("\n%s", s)
}); err != nil {
t.Error(err)
}
})
t.Run("TestFmtErrorf", func(t *testing.T) {
t.Parallel()
if err := stream.ForEach(
dirCmd(t, pkg.Dir, "git", "grep", "fmt\\.Errorf("),
func(s string) {
t.Errorf("\n%s <- please use \"errors.Errorf\" instead", s)
}); err != nil {
t.Error(err)
}
})
t.Run("TestOSIsErr", func(t *testing.T) {
t.Parallel()
if err := stream.ForEach(
dirCmd(t, pkg.Dir, "git", "grep", "os\\.Is"),
func(s string) {
t.Errorf("\n%s <- please use the \"oserror\" equivalent instead", s)
}); err != nil {
t.Error(err)
}
})
t.Run("TestSetFinalizer", func(t *testing.T) {
t.Parallel()
if err := stream.ForEach(
stream.Sequence(
dirCmd(t, pkg.Dir, "git", "grep", "-B1", "runtime\\.SetFinalizer("),
lintIgnore("lint:ignore SetFinalizer"),
stream.GrepNot(`^internal/invariants/finalizer_on.go`),
), func(s string) {
t.Errorf("\n%s <- please use the \"invariants.SetFinalizer\" equivalent instead", s)
}); err != nil {
t.Error(err)
}
})
// Disallow "raw" atomics; wrappers like atomic.Int32 provide much better
// safety and alignment guarantees.
t.Run("TestRawAtomics", func(t *testing.T) {
t.Parallel()
if err := stream.ForEach(
stream.Sequence(
dirCmd(t, pkg.Dir, "git", "grep", `atomic\.\(Load\|Store\|Add\|Swap\|Compare\)`),
lintIgnore("lint:ignore RawAtomics"),
), func(s string) {
t.Errorf("\n%s <- please use atomic wrappers (like atomic.Int32) instead", s)
}); err != nil {
t.Error(err)
}
})
t.Run("TestForbiddenImports", func(t *testing.T) {
t.Parallel()
// Forbidden-import-pkg -> permitted-replacement-pkg
forbiddenImports := map[string]string{
"errors": "github.com/cockroachdb/errors",
"pkg/errors": "github.com/cockroachdb/errors",
}
// grepBuf creates a grep string that matches any forbidden import pkgs.
var grepBuf bytes.Buffer
grepBuf.WriteByte('(')
for forbiddenPkg := range forbiddenImports {
grepBuf.WriteByte('|')
grepBuf.WriteString(regexp.QuoteMeta(forbiddenPkg))
}
grepBuf.WriteString(")$")
filter := stream.FilterFunc(func(arg stream.Arg) error {
for _, path := range pkgs {
buildContext := build.Default
buildContext.UseAllFiles = true
importPkg, err := buildContext.Import(path, pkg.Dir, 0)
if _, ok := err.(*build.MultiplePackageError); ok {
buildContext.UseAllFiles = false
importPkg, err = buildContext.Import(path, pkg.Dir, 0)
}
switch err.(type) {
case nil:
for _, s := range importPkg.Imports {
arg.Out <- importPkg.ImportPath + ": " + s
}
for _, s := range importPkg.TestImports {
arg.Out <- importPkg.ImportPath + ": " + s
}
for _, s := range importPkg.XTestImports {
arg.Out <- importPkg.ImportPath + ": " + s
}
case *build.NoGoError:
default:
return errors.Wrapf(err, "error loading package %s", path)
}
}
return nil
})
if err := stream.ForEach(stream.Sequence(
filter,
stream.Sort(),
stream.Uniq(),
stream.Grep(grepBuf.String()),
), func(s string) {
pkgStr := strings.Split(s, ": ")
importedPkg := pkgStr[1]
// Test that a disallowed package is not imported.
if replPkg, ok := forbiddenImports[importedPkg]; ok {
t.Errorf("\n%s <- please use %q instead of %q", s, replPkg, importedPkg)
}
}); err != nil {
t.Error(err)
}
})
t.Run("TestCrlfmt", func(t *testing.T) {
t.Parallel()
args := []string{"run", crlfmt, "-fast", "-tab", "2", "."}
var buf bytes.Buffer
if err := stream.ForEach(
stream.Sequence(
dirCmd(t, pkg.Dir, cmdGo, args...),
stream.GrepNot("go: downloading"),
),
func(s string) {
fmt.Fprintln(&buf, s)
}); err != nil {
t.Error(err)
}
errs := buf.String()
if len(errs) > 0 {
t.Errorf("\n%s", errs)
}
if t.Failed() {
reWriteCmd := []string{crlfmt, "-w"}
reWriteCmd = append(reWriteCmd, args...)
t.Logf("run the following to fix your formatting:\n"+
"\n%s\n\n"+
"Don't forget to add amend the result to the correct commits.",
strings.Join(reWriteCmd, " "),
)
}
})
}
// lintIgnore is a stream.FilterFunc that filters out lines that are preceded by
// the given ignore directive. The function assumes the input stream receives a
// sequence of strings that are to be considered as pairs. If the first string
// in the sequence matches the ignore directive, the following string is
// dropped, else it is emitted.
//
// For example, given the sequence "foo", "bar", "baz", "bam", and an ignore
// directive "foo", the sequence "baz", "bam" would be emitted. If the directive
// was "baz", the sequence "foo", "bar" would be emitted.
func lintIgnore(ignore string) stream.FilterFunc {
return func(arg stream.Arg) error {
var prev string
var i int
for s := range arg.In {
if i%2 == 0 {
// Fist string in the pair is used as the filter. Store it.
prev = s
} else {
// Second string is emitted only if it _does not_ match the directive.
if !strings.Contains(prev, ignore) {
arg.Out <- s
}
}
i++
}
return nil
}
}