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 }