1// Copyright 2019 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5//go:build ignore 6// +build ignore 7 8package main 9 10import ( 11 "archive/tar" 12 "archive/zip" 13 "bytes" 14 "compress/gzip" 15 "crypto/sha256" 16 "flag" 17 "fmt" 18 "io" 19 "io/ioutil" 20 "net/http" 21 "os" 22 "os/exec" 23 "path/filepath" 24 "regexp" 25 "runtime" 26 "strings" 27 "sync" 28 "testing" 29 "time" 30 31 "google.golang.org/protobuf/internal/version" 32) 33 34var ( 35 regenerate = flag.Bool("regenerate", false, "regenerate files") 36 buildRelease = flag.Bool("buildRelease", false, "build release binaries") 37 38 protobufVersion = "22.0" 39 protobufSHA256 = "" // ignored if protobufVersion is a git hash 40 41 golangVersions = func() []string { 42 var vers []string 43 switch runtime.GOOS + "/" + runtime.GOARCH { 44 case "darwin/arm64": 45 default: 46 vers = []string{"1.13.15", "1.14.15", "1.15.15"} 47 } 48 return append(vers, "1.16.15", "1.17.13", "1.18.10", "1.19.6") 49 }() 50 golangLatest = golangVersions[len(golangVersions)-1] 51 52 staticcheckVersion = "2022.1.2" 53 staticcheckSHA256s = map[string]string{ 54 "darwin/amd64": "baa35f8fb967ee2aacad57f026e3724fbf8d9b7ad8f682f4d44b2084a96e103b", 55 "darwin/arm64": "9f01a581eeea088d0a6272538360f6d84996d66ae554bfada8026fe24991daa0", 56 "linux/386": "4cf74373e5d668b265d7a241b59ba7d26064f2cd6af50b77e62c2b3e2f3afb43", 57 "linux/amd64": "6dbb7187e43812fa23363cdaaa90ab13544dd36e24d02e2347014e4cf265f06d", 58 } 59 60 // purgeTimeout determines the maximum age of unused sub-directories. 61 purgeTimeout = 30 * 24 * time.Hour // 1 month 62 63 // Variables initialized by mustInitDeps. 64 goPath string 65 modulePath string 66 protobufPath string 67) 68 69func Test(t *testing.T) { 70 mustInitDeps(t) 71 mustHandleFlags(t) 72 73 // Report dirt in the working tree quickly, rather than after 74 // going through all the presubmits. 75 // 76 // Fail the test late, so we can test uncommitted changes with -failfast. 77 gitDiff := mustRunCommand(t, "git", "diff", "HEAD") 78 if strings.TrimSpace(gitDiff) != "" { 79 fmt.Printf("WARNING: working tree contains uncommitted changes:\n%v\n", gitDiff) 80 } 81 gitUntracked := mustRunCommand(t, "git", "ls-files", "--others", "--exclude-standard") 82 if strings.TrimSpace(gitUntracked) != "" { 83 fmt.Printf("WARNING: working tree contains untracked files:\n%v\n", gitUntracked) 84 } 85 86 // Do the relatively fast checks up-front. 87 t.Run("GeneratedGoFiles", func(t *testing.T) { 88 diff := mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-types") 89 if strings.TrimSpace(diff) != "" { 90 t.Fatalf("stale generated files:\n%v", diff) 91 } 92 diff = mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-protos") 93 if strings.TrimSpace(diff) != "" { 94 t.Fatalf("stale generated files:\n%v", diff) 95 } 96 }) 97 t.Run("FormattedGoFiles", func(t *testing.T) { 98 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n") 99 diff := mustRunCommand(t, append([]string{"gofmt", "-d"}, files...)...) 100 if strings.TrimSpace(diff) != "" { 101 t.Fatalf("unformatted source files:\n%v", diff) 102 } 103 }) 104 t.Run("CopyrightHeaders", func(t *testing.T) { 105 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go", "*.proto")), "\n") 106 mustHaveCopyrightHeader(t, files) 107 }) 108 109 var wg sync.WaitGroup 110 sema := make(chan bool, (runtime.NumCPU()+1)/2) 111 for i := range golangVersions { 112 goVersion := golangVersions[i] 113 goLabel := "Go" + goVersion 114 runGo := func(label string, cmd command, args ...string) { 115 wg.Add(1) 116 sema <- true 117 go func() { 118 defer wg.Done() 119 defer func() { <-sema }() 120 t.Run(goLabel+"/"+label, func(t *testing.T) { 121 args[0] += goVersion 122 cmd.mustRun(t, args...) 123 }) 124 }() 125 } 126 127 workDir := filepath.Join(goPath, "src", modulePath) 128 runGo("Normal", command{Dir: workDir}, "go", "test", "-race", "./...") 129 runGo("PureGo", command{Dir: workDir}, "go", "test", "-race", "-tags", "purego", "./...") 130 runGo("Reflect", command{Dir: workDir}, "go", "test", "-race", "-tags", "protoreflect", "./...") 131 if goVersion == golangLatest { 132 runGo("ProtoLegacy", command{Dir: workDir}, "go", "test", "-race", "-tags", "protolegacy", "./...") 133 runGo("ProtocGenGo", command{Dir: "cmd/protoc-gen-go/testdata"}, "go", "test") 134 runGo("Conformance", command{Dir: "internal/conformance"}, "go", "test", "-execute") 135 136 // Only run the 32-bit compatibility tests for Linux; 137 // avoid Darwin since 10.15 dropped support i386 code execution. 138 if runtime.GOOS == "linux" { 139 runGo("Arch32Bit", command{Dir: workDir, Env: append(os.Environ(), "GOARCH=386")}, "go", "test", "./...") 140 } 141 } 142 } 143 wg.Wait() 144 145 t.Run("GoStaticCheck", func(t *testing.T) { 146 checks := []string{ 147 "all", // start with all checks enabled 148 "-SA1019", // disable deprecated usage check 149 "-S*", // disable code simplification checks 150 "-ST*", // disable coding style checks 151 "-U*", // disable unused declaration checks 152 } 153 out := mustRunCommand(t, "staticcheck", "-checks="+strings.Join(checks, ","), "-fail=none", "./...") 154 155 // Filter out findings from certain paths. 156 var findings []string 157 for _, finding := range strings.Split(strings.TrimSpace(out), "\n") { 158 switch { 159 case strings.HasPrefix(finding, "internal/testprotos/legacy/"): 160 default: 161 findings = append(findings, finding) 162 } 163 } 164 if len(findings) > 0 { 165 t.Fatalf("staticcheck findings:\n%v", strings.Join(findings, "\n")) 166 } 167 }) 168 t.Run("CommittedGitChanges", func(t *testing.T) { 169 if strings.TrimSpace(gitDiff) != "" { 170 t.Fatalf("uncommitted changes") 171 } 172 }) 173 t.Run("TrackedGitFiles", func(t *testing.T) { 174 if strings.TrimSpace(gitUntracked) != "" { 175 t.Fatalf("untracked files") 176 } 177 }) 178} 179 180func mustInitDeps(t *testing.T) { 181 check := func(err error) { 182 t.Helper() 183 if err != nil { 184 t.Fatal(err) 185 } 186 } 187 188 // Determine the directory to place the test directory. 189 repoRoot, err := os.Getwd() 190 check(err) 191 testDir := filepath.Join(repoRoot, ".cache") 192 check(os.MkdirAll(testDir, 0775)) 193 194 // Delete the current directory if non-empty, 195 // which only occurs if a dependency failed to initialize properly. 196 var workingDir string 197 finishedDirs := map[string]bool{} 198 defer func() { 199 if workingDir != "" { 200 os.RemoveAll(workingDir) // best-effort 201 } 202 }() 203 startWork := func(name string) string { 204 workingDir = filepath.Join(testDir, name) 205 return workingDir 206 } 207 finishWork := func() { 208 finishedDirs[workingDir] = true 209 workingDir = "" 210 } 211 212 // Delete other sub-directories that are no longer relevant. 213 defer func() { 214 now := time.Now() 215 fis, _ := ioutil.ReadDir(testDir) 216 for _, fi := range fis { 217 dir := filepath.Join(testDir, fi.Name()) 218 if finishedDirs[dir] { 219 os.Chtimes(dir, now, now) // best-effort 220 continue 221 } 222 if now.Sub(fi.ModTime()) < purgeTimeout { 223 continue 224 } 225 fmt.Printf("delete %v\n", fi.Name()) 226 os.RemoveAll(dir) // best-effort 227 } 228 }() 229 230 // The bin directory contains symlinks to each tool by version. 231 // It is safe to delete this directory and run the test script from scratch. 232 binPath := startWork("bin") 233 check(os.RemoveAll(binPath)) 234 check(os.Mkdir(binPath, 0775)) 235 check(os.Setenv("PATH", binPath+":"+os.Getenv("PATH"))) 236 registerBinary := func(name, path string) { 237 check(os.Symlink(path, filepath.Join(binPath, name))) 238 } 239 finishWork() 240 241 // Download and build the protobuf toolchain. 242 // We avoid downloading the pre-compiled binaries since they do not contain 243 // the conformance test runner. 244 protobufPath = startWork("protobuf-" + protobufVersion) 245 if _, err := os.Stat(protobufPath); err != nil { 246 fmt.Printf("download %v\n", filepath.Base(protobufPath)) 247 checkoutVersion := protobufVersion 248 if isCommit := strings.Trim(protobufVersion, "0123456789abcdef") == ""; !isCommit { 249 // release tags have "v" prefix 250 checkoutVersion = "v" + protobufVersion 251 } 252 command{Dir: testDir}.mustRun(t, "git", "clone", "https://github.com/protocolbuffers/protobuf", "protobuf-"+protobufVersion) 253 command{Dir: protobufPath}.mustRun(t, "git", "checkout", checkoutVersion) 254 255 fmt.Printf("build %v\n", filepath.Base(protobufPath)) 256 command{Dir: protobufPath}.mustRun(t, "bazel", "build", ":protoc", "//conformance:conformance_test_runner") 257 } 258 check(os.Setenv("PROTOBUF_ROOT", protobufPath)) // for generate-protos 259 registerBinary("conform-test-runner", filepath.Join(protobufPath, "bazel-bin", "conformance", "conformance_test_runner")) 260 registerBinary("protoc", filepath.Join(protobufPath, "bazel-bin", "protoc")) 261 finishWork() 262 263 // Download each Go toolchain version. 264 for _, v := range golangVersions { 265 goDir := startWork("go" + v) 266 if _, err := os.Stat(goDir); err != nil { 267 fmt.Printf("download %v\n", filepath.Base(goDir)) 268 url := fmt.Sprintf("https://dl.google.com/go/go%v.%v-%v.tar.gz", v, runtime.GOOS, runtime.GOARCH) 269 downloadArchive(check, goDir, url, "go", "") // skip SHA256 check as we fetch over https from a trusted domain 270 } 271 registerBinary("go"+v, filepath.Join(goDir, "bin", "go")) 272 finishWork() 273 } 274 registerBinary("go", filepath.Join(testDir, "go"+golangLatest, "bin", "go")) 275 registerBinary("gofmt", filepath.Join(testDir, "go"+golangLatest, "bin", "gofmt")) 276 277 // Download the staticcheck tool. 278 checkDir := startWork("staticcheck-" + staticcheckVersion) 279 if _, err := os.Stat(checkDir); err != nil { 280 fmt.Printf("download %v\n", filepath.Base(checkDir)) 281 url := fmt.Sprintf("https://github.com/dominikh/go-tools/releases/download/%v/staticcheck_%v_%v.tar.gz", staticcheckVersion, runtime.GOOS, runtime.GOARCH) 282 downloadArchive(check, checkDir, url, "staticcheck", staticcheckSHA256s[runtime.GOOS+"/"+runtime.GOARCH]) 283 } 284 registerBinary("staticcheck", filepath.Join(checkDir, "staticcheck")) 285 finishWork() 286 287 // GitHub actions sets GOROOT, which confuses invocations of the Go toolchain. 288 // Explicitly clear GOROOT, so each toolchain uses their default GOROOT. 289 check(os.Unsetenv("GOROOT")) 290 291 // Set a cache directory outside the test directory. 292 check(os.Setenv("GOCACHE", filepath.Join(repoRoot, ".gocache"))) 293 294 // Setup GOPATH for pre-module support (i.e., go1.10 and earlier). 295 goPath = startWork("gopath") 296 modulePath = strings.TrimSpace(command{Dir: testDir}.mustRun(t, "go", "list", "-m", "-f", "{{.Path}}")) 297 check(os.RemoveAll(filepath.Join(goPath, "src"))) 298 check(os.MkdirAll(filepath.Join(goPath, "src", filepath.Dir(modulePath)), 0775)) 299 check(os.Symlink(repoRoot, filepath.Join(goPath, "src", modulePath))) 300 command{Dir: repoRoot}.mustRun(t, "go", "mod", "tidy") 301 command{Dir: repoRoot}.mustRun(t, "go", "mod", "vendor") 302 check(os.Setenv("GOPATH", goPath)) 303 finishWork() 304} 305 306func downloadFile(check func(error), dstPath, srcURL string) { 307 resp, err := http.Get(srcURL) 308 check(err) 309 defer resp.Body.Close() 310 311 check(os.MkdirAll(filepath.Dir(dstPath), 0775)) 312 f, err := os.Create(dstPath) 313 check(err) 314 315 _, err = io.Copy(f, resp.Body) 316 check(err) 317} 318 319func downloadArchive(check func(error), dstPath, srcURL, skipPrefix, wantSHA256 string) { 320 check(os.RemoveAll(dstPath)) 321 322 resp, err := http.Get(srcURL) 323 check(err) 324 defer resp.Body.Close() 325 326 var r io.Reader = resp.Body 327 if wantSHA256 != "" { 328 b, err := ioutil.ReadAll(resp.Body) 329 check(err) 330 r = bytes.NewReader(b) 331 332 if gotSHA256 := fmt.Sprintf("%x", sha256.Sum256(b)); gotSHA256 != wantSHA256 { 333 check(fmt.Errorf("checksum validation error:\ngot %v\nwant %v", gotSHA256, wantSHA256)) 334 } 335 } 336 337 zr, err := gzip.NewReader(r) 338 check(err) 339 340 tr := tar.NewReader(zr) 341 for { 342 h, err := tr.Next() 343 if err == io.EOF { 344 return 345 } 346 check(err) 347 348 // Skip directories or files outside the prefix directory. 349 if len(skipPrefix) > 0 { 350 if !strings.HasPrefix(h.Name, skipPrefix) { 351 continue 352 } 353 if len(h.Name) > len(skipPrefix) && h.Name[len(skipPrefix)] != '/' { 354 continue 355 } 356 } 357 358 path := strings.TrimPrefix(strings.TrimPrefix(h.Name, skipPrefix), "/") 359 path = filepath.Join(dstPath, filepath.FromSlash(path)) 360 mode := os.FileMode(h.Mode & 0777) 361 switch h.Typeflag { 362 case tar.TypeReg: 363 b, err := ioutil.ReadAll(tr) 364 check(err) 365 check(ioutil.WriteFile(path, b, mode)) 366 case tar.TypeDir: 367 check(os.Mkdir(path, mode)) 368 } 369 } 370} 371 372func mustHandleFlags(t *testing.T) { 373 if *regenerate { 374 t.Run("Generate", func(t *testing.T) { 375 fmt.Print(mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-types", "-execute")) 376 fmt.Print(mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-protos", "-execute")) 377 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n") 378 mustRunCommand(t, append([]string{"gofmt", "-w"}, files...)...) 379 }) 380 } 381 if *buildRelease { 382 t.Run("BuildRelease", func(t *testing.T) { 383 v := version.String() 384 for _, goos := range []string{"linux", "darwin", "windows"} { 385 for _, goarch := range []string{"386", "amd64", "arm64"} { 386 // Avoid Darwin since 10.15 dropped support for i386. 387 if goos == "darwin" && goarch == "386" { 388 continue 389 } 390 391 binPath := filepath.Join("bin", fmt.Sprintf("protoc-gen-go.%v.%v.%v", v, goos, goarch)) 392 393 // Build the binary. 394 cmd := command{Env: append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)} 395 cmd.mustRun(t, "go", "build", "-trimpath", "-ldflags", "-s -w -buildid=", "-o", binPath, "./cmd/protoc-gen-go") 396 397 // Archive and compress the binary. 398 in, err := ioutil.ReadFile(binPath) 399 if err != nil { 400 t.Fatal(err) 401 } 402 out := new(bytes.Buffer) 403 suffix := "" 404 comment := fmt.Sprintf("protoc-gen-go VERSION=%v GOOS=%v GOARCH=%v", v, goos, goarch) 405 switch goos { 406 case "windows": 407 suffix = ".zip" 408 zw := zip.NewWriter(out) 409 zw.SetComment(comment) 410 fw, _ := zw.Create("protoc-gen-go.exe") 411 fw.Write(in) 412 zw.Close() 413 default: 414 suffix = ".tar.gz" 415 gz, _ := gzip.NewWriterLevel(out, gzip.BestCompression) 416 gz.Comment = comment 417 tw := tar.NewWriter(gz) 418 tw.WriteHeader(&tar.Header{ 419 Name: "protoc-gen-go", 420 Mode: int64(0775), 421 Size: int64(len(in)), 422 }) 423 tw.Write(in) 424 tw.Close() 425 gz.Close() 426 } 427 if err := ioutil.WriteFile(binPath+suffix, out.Bytes(), 0664); err != nil { 428 t.Fatal(err) 429 } 430 } 431 } 432 }) 433 } 434 if *regenerate || *buildRelease { 435 t.SkipNow() 436 } 437} 438 439var copyrightRegex = []*regexp.Regexp{ 440 regexp.MustCompile(`^// Copyright \d\d\d\d The Go Authors\. All rights reserved. 441// Use of this source code is governed by a BSD-style 442// license that can be found in the LICENSE file\. 443`), 444 // Generated .pb.go files from main protobuf repo. 445 regexp.MustCompile(`^// Protocol Buffers - Google's data interchange format 446// Copyright \d\d\d\d Google Inc\. All rights reserved\. 447`), 448} 449 450func mustHaveCopyrightHeader(t *testing.T, files []string) { 451 var bad []string 452File: 453 for _, file := range files { 454 b, err := ioutil.ReadFile(file) 455 if err != nil { 456 t.Fatal(err) 457 } 458 for _, re := range copyrightRegex { 459 if loc := re.FindIndex(b); loc != nil && loc[0] == 0 { 460 continue File 461 } 462 } 463 bad = append(bad, file) 464 } 465 if len(bad) > 0 { 466 t.Fatalf("files with missing/bad copyright headers:\n %v", strings.Join(bad, "\n ")) 467 } 468} 469 470type command struct { 471 Dir string 472 Env []string 473} 474 475func (c command) mustRun(t *testing.T, args ...string) string { 476 t.Helper() 477 stdout := new(bytes.Buffer) 478 stderr := new(bytes.Buffer) 479 cmd := exec.Command(args[0], args[1:]...) 480 cmd.Dir = "." 481 if c.Dir != "" { 482 cmd.Dir = c.Dir 483 } 484 cmd.Env = os.Environ() 485 if c.Env != nil { 486 cmd.Env = c.Env 487 } 488 cmd.Env = append(cmd.Env, "PWD="+cmd.Dir) 489 cmd.Stdout = stdout 490 cmd.Stderr = stderr 491 if err := cmd.Run(); err != nil { 492 t.Fatalf("executing (%v): %v\n%s%s", strings.Join(args, " "), err, stdout.String(), stderr.String()) 493 } 494 return stdout.String() 495} 496 497func mustRunCommand(t *testing.T, args ...string) string { 498 t.Helper() 499 return command{}.mustRun(t, args...) 500} 501