github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/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/ghemawat/stream" 19 "github.com/hashicorp/go-version" 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@2022.1.2" 27 crlfmt = "github.com/cockroachdb/crlfmt@b3eff0b" 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 53 const root = "github.com/zuoyebang/bitalostable" 54 55 pkg, err := build.Import(root, "../..", 0) 56 require.NoError(t, err) 57 58 var pkgs []string 59 if err := stream.ForEach( 60 stream.Sequence( 61 dirCmd(t, pkg.Dir, "go", "list", "./..."), 62 ignoreGoMod(), 63 ), func(s string) { 64 pkgs = append(pkgs, s) 65 }); err != nil { 66 require.NoError(t, err) 67 } 68 69 t.Run("TestGolint", func(t *testing.T) { 70 // Go versions less than 1.17 do not support the `go run path@version` 71 // syntax. 72 // TODO(travers): This can be removed when support for go1.16 is dropped. 73 if goVersionMatches("< 1.17") { 74 t.Skip("go run path@version unsupported in versions < go1.17") 75 } 76 t.Parallel() 77 78 args := []string{"run", golint} 79 args = append(args, pkgs...) 80 81 // This is overkill right now, but provides a structure for filtering out 82 // lint errors we don't care about. 83 if err := stream.ForEach( 84 stream.Sequence( 85 dirCmd(t, pkg.Dir, cmdGo, args...), 86 stream.GrepNot("go: downloading"), 87 ), func(s string) { 88 t.Errorf("\n%s", s) 89 }); err != nil { 90 t.Error(err) 91 } 92 }) 93 94 t.Run("TestStaticcheck", func(t *testing.T) { 95 // Go versions less than 1.17 do not support the `go run path@version` 96 // syntax. 97 // TODO(travers): This can be removed when support for go1.16 is dropped. 98 if goVersionMatches("< 1.17") { 99 t.Skip("go run path@version unsupported in versions < go1.17") 100 } 101 t.Parallel() 102 103 args := []string{"run", staticcheck} 104 args = append(args, pkgs...) 105 106 if err := stream.ForEach( 107 stream.Sequence( 108 dirCmd(t, pkg.Dir, cmdGo, args...), 109 stream.GrepNot("go: downloading"), 110 ), func(s string) { 111 t.Errorf("\n%s", s) 112 }); err != nil { 113 t.Error(err) 114 } 115 }) 116 117 t.Run("TestGoVet", func(t *testing.T) { 118 t.Parallel() 119 120 if err := stream.ForEach( 121 stream.Sequence( 122 dirCmd(t, pkg.Dir, "go", "vet", "-all", "./..."), 123 stream.GrepNot(`^#`), // ignore comment lines 124 ignoreGoMod(), 125 ), func(s string) { 126 t.Errorf("\n%s", s) 127 }); err != nil { 128 t.Error(err) 129 } 130 }) 131 132 t.Run("TestFmtErrorf", func(t *testing.T) { 133 t.Parallel() 134 135 if err := stream.ForEach( 136 stream.Sequence( 137 dirCmd(t, pkg.Dir, "git", "grep", "fmt\\.Errorf("), 138 stream.GrepNot(`^vendor/`), // ignore vendor 139 ), func(s string) { 140 t.Errorf("\n%s <- please use \"errors.Errorf\" instead", s) 141 }); err != nil { 142 t.Error(err) 143 } 144 }) 145 146 t.Run("TestOSIsErr", func(t *testing.T) { 147 t.Parallel() 148 149 if err := stream.ForEach( 150 stream.Sequence( 151 dirCmd(t, pkg.Dir, "git", "grep", "os\\.Is"), 152 stream.GrepNot(`^vendor/`), // ignore vendor 153 ), func(s string) { 154 t.Errorf("\n%s <- please use the \"oserror\" equivalent instead", s) 155 }); err != nil { 156 t.Error(err) 157 } 158 }) 159 160 t.Run("TestSetFinalizer", func(t *testing.T) { 161 t.Parallel() 162 163 if err := stream.ForEach( 164 stream.Sequence( 165 dirCmd(t, pkg.Dir, "git", "grep", "-B1", "runtime\\.SetFinalizer("), 166 lintIgnore("lint:ignore SetFinalizer"), 167 stream.GrepNot(`^vendor/`), // ignore vendor 168 stream.GrepNot(`^internal/invariants/finalizer_on.go`), 169 ), func(s string) { 170 t.Errorf("\n%s <- please use the \"invariants.SetFinalizer\" equivalent instead", s) 171 }); err != nil { 172 t.Error(err) 173 } 174 }) 175 176 t.Run("TestForbiddenImports", func(t *testing.T) { 177 t.Parallel() 178 179 // Forbidden-import-pkg -> permitted-replacement-pkg 180 forbiddenImports := map[string]string{ 181 "errors": "github.com/cockroachdb/errors", 182 "pkg/errors": "github.com/cockroachdb/errors", 183 } 184 185 // grepBuf creates a grep string that matches any forbidden import pkgs. 186 var grepBuf bytes.Buffer 187 grepBuf.WriteByte('(') 188 for forbiddenPkg := range forbiddenImports { 189 grepBuf.WriteByte('|') 190 grepBuf.WriteString(regexp.QuoteMeta(forbiddenPkg)) 191 } 192 grepBuf.WriteString(")$") 193 194 filter := stream.FilterFunc(func(arg stream.Arg) error { 195 for _, path := range pkgs { 196 buildContext := build.Default 197 buildContext.UseAllFiles = true 198 importPkg, err := buildContext.Import(path, pkg.Dir, 0) 199 if _, ok := err.(*build.MultiplePackageError); ok { 200 buildContext.UseAllFiles = false 201 importPkg, err = buildContext.Import(path, pkg.Dir, 0) 202 } 203 204 switch err.(type) { 205 case nil: 206 for _, s := range importPkg.Imports { 207 arg.Out <- importPkg.ImportPath + ": " + s 208 } 209 for _, s := range importPkg.TestImports { 210 arg.Out <- importPkg.ImportPath + ": " + s 211 } 212 for _, s := range importPkg.XTestImports { 213 arg.Out <- importPkg.ImportPath + ": " + s 214 } 215 case *build.NoGoError: 216 default: 217 return errors.Wrapf(err, "error loading package %s", path) 218 } 219 } 220 return nil 221 }) 222 if err := stream.ForEach(stream.Sequence( 223 filter, 224 stream.Sort(), 225 stream.Uniq(), 226 stream.Grep(grepBuf.String()), 227 ), func(s string) { 228 pkgStr := strings.Split(s, ": ") 229 importedPkg := pkgStr[1] 230 231 // Test that a disallowed package is not imported. 232 if replPkg, ok := forbiddenImports[importedPkg]; ok { 233 t.Errorf("\n%s <- please use %q instead of %q", s, replPkg, importedPkg) 234 } 235 }); err != nil { 236 t.Error(err) 237 } 238 }) 239 240 t.Run("TestCrlfmt", func(t *testing.T) { 241 // Go versions less than 1.17 do not support the `go run path@version` 242 // syntax. 243 // TODO(travers): This can be removed when support for go1.16 is dropped. 244 if goVersionMatches("< 1.17") { 245 t.Skip("go run path@version unsupported in versions < go1.17") 246 } 247 t.Parallel() 248 249 args := []string{"run", crlfmt, "-fast", "-tab", "2", "-ignore", "^vendor/", "."} 250 var buf bytes.Buffer 251 if err := stream.ForEach( 252 stream.Sequence( 253 dirCmd(t, pkg.Dir, cmdGo, args...), 254 stream.GrepNot("go: downloading"), 255 ), 256 func(s string) { 257 fmt.Fprintln(&buf, s) 258 }); err != nil { 259 t.Error(err) 260 } 261 errs := buf.String() 262 if len(errs) > 0 { 263 t.Errorf("\n%s", errs) 264 } 265 266 if t.Failed() { 267 reWriteCmd := []string{crlfmt, "-w"} 268 reWriteCmd = append(reWriteCmd, args...) 269 t.Logf("run the following to fix your formatting:\n"+ 270 "\n%s\n\n"+ 271 "Don't forget to add amend the result to the correct commits.", 272 strings.Join(reWriteCmd, " "), 273 ) 274 } 275 }) 276 } 277 278 // lintIgnore is a stream.FilterFunc that filters out lines that are preceded by 279 // the given ignore directive. The function assumes the input stream receives a 280 // sequence of strings that are to be considered as pairs. If the first string 281 // in the sequence matches the ignore directive, the following string is 282 // dropped, else it is emitted. 283 // 284 // For example, given the sequence "foo", "bar", "baz", "bam", and an ignore 285 // directive "foo", the sequence "baz", "bam" would be emitted. If the directive 286 // was "baz", the sequence "foo", "bar" would be emitted. 287 func lintIgnore(ignore string) stream.FilterFunc { 288 return func(arg stream.Arg) error { 289 var prev string 290 var i int 291 for s := range arg.In { 292 if i%2 == 0 { 293 // Fist string in the pair is used as the filter. Store it. 294 prev = s 295 } else { 296 // Second string is emitted only if it _does not_ match the directive. 297 if !strings.Contains(prev, ignore) { 298 arg.Out <- s 299 } 300 } 301 i++ 302 } 303 return nil 304 } 305 } 306 307 // goVersionMatches returns true if the Go runtime versions matches the given 308 // semver constraint. If the runtime version does not contain a semver string 309 // (i.e. it is a SHA), this function returns false. 310 func goVersionMatches(constraint string) bool { 311 runtimeVersion := strings.TrimPrefix(runtime.Version(), "go") 312 goV, err := version.NewSemver(runtimeVersion) 313 if err != nil { 314 return false // Fail open. 315 } 316 c := version.MustConstraints(version.NewConstraint(constraint)) 317 return c.Check(goV) 318 }