github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/internal/lint/lint_test.go (about)

     1  // Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
     2  // of this source code is governed by a BSD-style license that can be found in
     3  // the LICENSE file.
     4  
     5  package lint
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"go/build"
    11  	"os/exec"
    12  	"regexp"
    13  	"runtime"
    14  	"strings"
    15  	"testing"
    16  
    17  	"github.com/cockroachdb/errors"
    18  	"github.com/cockroachdb/pebble/internal/invariants"
    19  	"github.com/ghemawat/stream"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  const (
    24  	cmdGo       = "go"
    25  	golint      = "golang.org/x/lint/golint@6edffad5e6160f5949cdefc81710b2706fbcd4f6"
    26  	staticcheck = "honnef.co/go/tools/cmd/staticcheck@2023.1"
    27  	crlfmt      = "github.com/cockroachdb/crlfmt@44a36ec7"
    28  )
    29  
    30  func dirCmd(t *testing.T, dir string, name string, args ...string) stream.Filter {
    31  	cmd := exec.Command(name, args...)
    32  	cmd.Dir = dir
    33  	out, err := cmd.CombinedOutput()
    34  	switch err.(type) {
    35  	case nil:
    36  	case *exec.ExitError:
    37  		// Non-zero exit is expected.
    38  	default:
    39  		require.NoError(t, err)
    40  	}
    41  	return stream.ReadLines(bytes.NewReader(out))
    42  }
    43  
    44  func ignoreGoMod() stream.Filter {
    45  	return stream.GrepNot(`^go: (finding|extracting|downloading)`)
    46  }
    47  
    48  func TestLint(t *testing.T) {
    49  	if runtime.GOOS == "windows" {
    50  		t.Skip("lint checks skipped on Windows")
    51  	}
    52  	if invariants.RaceEnabled {
    53  		// We are not interested in race-testing the linters themselves.
    54  		t.Skip("lint checks skipped on race builds")
    55  	}
    56  
    57  	const root = "github.com/cockroachdb/pebble"
    58  
    59  	pkg, err := build.Import(root, "../..", 0)
    60  	require.NoError(t, err)
    61  
    62  	var pkgs []string
    63  	if err := stream.ForEach(
    64  		stream.Sequence(
    65  			dirCmd(t, pkg.Dir, "go", "list", "./..."),
    66  			ignoreGoMod(),
    67  		), func(s string) {
    68  			pkgs = append(pkgs, s)
    69  		}); err != nil {
    70  		require.NoError(t, err)
    71  	}
    72  
    73  	t.Run("TestGolint", func(t *testing.T) {
    74  		t.Parallel()
    75  
    76  		args := []string{"run", golint}
    77  		args = append(args, pkgs...)
    78  
    79  		// This is overkill right now, but provides a structure for filtering out
    80  		// lint errors we don't care about.
    81  		if err := stream.ForEach(
    82  			stream.Sequence(
    83  				dirCmd(t, pkg.Dir, cmdGo, args...),
    84  				stream.GrepNot("go: downloading"),
    85  			), func(s string) {
    86  				t.Errorf("\n%s", s)
    87  			}); err != nil {
    88  			t.Error(err)
    89  		}
    90  	})
    91  
    92  	t.Run("TestStaticcheck", func(t *testing.T) {
    93  		t.Parallel()
    94  
    95  		args := []string{"run", staticcheck}
    96  		args = append(args, pkgs...)
    97  
    98  		if err := stream.ForEach(
    99  			stream.Sequence(
   100  				dirCmd(t, pkg.Dir, cmdGo, args...),
   101  				stream.GrepNot("go: downloading"),
   102  			), func(s string) {
   103  				t.Errorf("\n%s", s)
   104  			}); err != nil {
   105  			t.Error(err)
   106  		}
   107  	})
   108  
   109  	t.Run("TestGoVet", func(t *testing.T) {
   110  		t.Parallel()
   111  
   112  		if err := stream.ForEach(
   113  			stream.Sequence(
   114  				dirCmd(t, pkg.Dir, "go", "vet", "-all", "./..."),
   115  				stream.GrepNot(`^#`), // ignore comment lines
   116  				ignoreGoMod(),
   117  			), func(s string) {
   118  				t.Errorf("\n%s", s)
   119  			}); err != nil {
   120  			t.Error(err)
   121  		}
   122  	})
   123  
   124  	t.Run("TestFmtErrorf", func(t *testing.T) {
   125  		t.Parallel()
   126  
   127  		if err := stream.ForEach(
   128  			dirCmd(t, pkg.Dir, "git", "grep", "fmt\\.Errorf("),
   129  			func(s string) {
   130  				t.Errorf("\n%s <- please use \"errors.Errorf\" instead", s)
   131  			}); err != nil {
   132  			t.Error(err)
   133  		}
   134  	})
   135  
   136  	t.Run("TestOSIsErr", func(t *testing.T) {
   137  		t.Parallel()
   138  
   139  		if err := stream.ForEach(
   140  			dirCmd(t, pkg.Dir, "git", "grep", "os\\.Is"),
   141  			func(s string) {
   142  				t.Errorf("\n%s <- please use the \"oserror\" equivalent instead", s)
   143  			}); err != nil {
   144  			t.Error(err)
   145  		}
   146  	})
   147  
   148  	t.Run("TestSetFinalizer", func(t *testing.T) {
   149  		t.Parallel()
   150  
   151  		if err := stream.ForEach(
   152  			stream.Sequence(
   153  				dirCmd(t, pkg.Dir, "git", "grep", "-B1", "runtime\\.SetFinalizer("),
   154  				lintIgnore("lint:ignore SetFinalizer"),
   155  				stream.GrepNot(`^internal/invariants/finalizer_on.go`),
   156  			), func(s string) {
   157  				t.Errorf("\n%s <- please use the \"invariants.SetFinalizer\" equivalent instead", s)
   158  			}); err != nil {
   159  			t.Error(err)
   160  		}
   161  	})
   162  
   163  	// Disallow "raw" atomics; wrappers like atomic.Int32 provide much better
   164  	// safety and alignment guarantees.
   165  	t.Run("TestRawAtomics", func(t *testing.T) {
   166  		t.Parallel()
   167  		if err := stream.ForEach(
   168  			stream.Sequence(
   169  				dirCmd(t, pkg.Dir, "git", "grep", `atomic\.\(Load\|Store\|Add\|Swap\|Compare\)`),
   170  				lintIgnore("lint:ignore RawAtomics"),
   171  			), func(s string) {
   172  				t.Errorf("\n%s <- please use atomic wrappers (like atomic.Int32) instead", s)
   173  			}); err != nil {
   174  			t.Error(err)
   175  		}
   176  	})
   177  
   178  	t.Run("TestForbiddenImports", func(t *testing.T) {
   179  		t.Parallel()
   180  
   181  		// Forbidden-import-pkg -> permitted-replacement-pkg
   182  		forbiddenImports := map[string]string{
   183  			"errors":     "github.com/cockroachdb/errors",
   184  			"pkg/errors": "github.com/cockroachdb/errors",
   185  		}
   186  
   187  		// grepBuf creates a grep string that matches any forbidden import pkgs.
   188  		var grepBuf bytes.Buffer
   189  		grepBuf.WriteByte('(')
   190  		for forbiddenPkg := range forbiddenImports {
   191  			grepBuf.WriteByte('|')
   192  			grepBuf.WriteString(regexp.QuoteMeta(forbiddenPkg))
   193  		}
   194  		grepBuf.WriteString(")$")
   195  
   196  		filter := stream.FilterFunc(func(arg stream.Arg) error {
   197  			for _, path := range pkgs {
   198  				buildContext := build.Default
   199  				buildContext.UseAllFiles = true
   200  				importPkg, err := buildContext.Import(path, pkg.Dir, 0)
   201  				if _, ok := err.(*build.MultiplePackageError); ok {
   202  					buildContext.UseAllFiles = false
   203  					importPkg, err = buildContext.Import(path, pkg.Dir, 0)
   204  				}
   205  
   206  				switch err.(type) {
   207  				case nil:
   208  					for _, s := range importPkg.Imports {
   209  						arg.Out <- importPkg.ImportPath + ": " + s
   210  					}
   211  					for _, s := range importPkg.TestImports {
   212  						arg.Out <- importPkg.ImportPath + ": " + s
   213  					}
   214  					for _, s := range importPkg.XTestImports {
   215  						arg.Out <- importPkg.ImportPath + ": " + s
   216  					}
   217  				case *build.NoGoError:
   218  				default:
   219  					return errors.Wrapf(err, "error loading package %s", path)
   220  				}
   221  			}
   222  			return nil
   223  		})
   224  		if err := stream.ForEach(stream.Sequence(
   225  			filter,
   226  			stream.Sort(),
   227  			stream.Uniq(),
   228  			stream.Grep(grepBuf.String()),
   229  		), func(s string) {
   230  			pkgStr := strings.Split(s, ": ")
   231  			importedPkg := pkgStr[1]
   232  
   233  			// Test that a disallowed package is not imported.
   234  			if replPkg, ok := forbiddenImports[importedPkg]; ok {
   235  				t.Errorf("\n%s <- please use %q instead of %q", s, replPkg, importedPkg)
   236  			}
   237  		}); err != nil {
   238  			t.Error(err)
   239  		}
   240  	})
   241  
   242  	t.Run("TestCrlfmt", func(t *testing.T) {
   243  		t.Parallel()
   244  
   245  		args := []string{"run", crlfmt, "-fast", "-tab", "2", "."}
   246  		var buf bytes.Buffer
   247  		if err := stream.ForEach(
   248  			stream.Sequence(
   249  				dirCmd(t, pkg.Dir, cmdGo, args...),
   250  				stream.GrepNot("go: downloading"),
   251  			),
   252  			func(s string) {
   253  				fmt.Fprintln(&buf, s)
   254  			}); err != nil {
   255  			t.Error(err)
   256  		}
   257  		errs := buf.String()
   258  		if len(errs) > 0 {
   259  			t.Errorf("\n%s", errs)
   260  		}
   261  
   262  		if t.Failed() {
   263  			reWriteCmd := []string{crlfmt, "-w"}
   264  			reWriteCmd = append(reWriteCmd, args...)
   265  			t.Logf("run the following to fix your formatting:\n"+
   266  				"\n%s\n\n"+
   267  				"Don't forget to add amend the result to the correct commits.",
   268  				strings.Join(reWriteCmd, " "),
   269  			)
   270  		}
   271  	})
   272  }
   273  
   274  // lintIgnore is a stream.FilterFunc that filters out lines that are preceded by
   275  // the given ignore directive. The function assumes the input stream receives a
   276  // sequence of strings that are to be considered as pairs. If the first string
   277  // in the sequence matches the ignore directive, the following string is
   278  // dropped, else it is emitted.
   279  //
   280  // For example, given the sequence "foo", "bar", "baz", "bam", and an ignore
   281  // directive "foo", the sequence "baz", "bam" would be emitted. If the directive
   282  // was "baz", the sequence "foo", "bar" would be emitted.
   283  func lintIgnore(ignore string) stream.FilterFunc {
   284  	return func(arg stream.Arg) error {
   285  		var prev string
   286  		var i int
   287  		for s := range arg.In {
   288  			if i%2 == 0 {
   289  				// Fist string in the pair is used as the filter. Store it.
   290  				prev = s
   291  			} else {
   292  				// Second string is emitted only if it _does not_ match the directive.
   293  				if !strings.Contains(prev, ignore) {
   294  					arg.Out <- s
   295  				}
   296  			}
   297  			i++
   298  		}
   299  		return nil
   300  	}
   301  }