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