github.com/whiteCcinn/protobuf-go@v1.0.9/integration_test.go (about) 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 8 package main 9 10 import ( 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 "github.com/whiteCcinn/protobuf-go/internal/version" 32 ) 33 34 var ( 35 regenerate = flag.Bool("regenerate", false, "regenerate files") 36 buildRelease = flag.Bool("buildRelease", false, "build release binaries") 37 38 protobufVersion = "21.5" 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.12", "1.18.4") 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 69 func 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 simplication 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 180 func 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 if isCommit := strings.Trim(protobufVersion, "0123456789abcdef") == ""; isCommit { 248 command{Dir: testDir}.mustRun(t, "git", "clone", "https://github.com/protocolbuffers/protobuf", "protobuf-"+protobufVersion) 249 command{Dir: protobufPath}.mustRun(t, "git", "checkout", protobufVersion) 250 } else { 251 url := fmt.Sprintf("https://github.com/google/protobuf/releases/download/v%v/protobuf-all-%v.tar.gz", protobufVersion, protobufVersion) 252 downloadArchive(check, protobufPath, url, "protobuf-"+protobufVersion, protobufSHA256) 253 } 254 255 fmt.Printf("build %v\n", filepath.Base(protobufPath)) 256 command{Dir: protobufPath}.mustRun(t, "./autogen.sh") 257 command{Dir: protobufPath}.mustRun(t, "./configure") 258 command{Dir: protobufPath}.mustRun(t, "make") 259 command{Dir: filepath.Join(protobufPath, "conformance")}.mustRun(t, "make") 260 } 261 check(os.Setenv("PROTOBUF_ROOT", protobufPath)) // for generate-protos 262 registerBinary("conform-test-runner", filepath.Join(protobufPath, "conformance", "conformance-test-runner")) 263 registerBinary("protoc", filepath.Join(protobufPath, "src", "protoc")) 264 finishWork() 265 266 // Download each Go toolchain version. 267 for _, v := range golangVersions { 268 goDir := startWork("go" + v) 269 if _, err := os.Stat(goDir); err != nil { 270 fmt.Printf("download %v\n", filepath.Base(goDir)) 271 url := fmt.Sprintf("https://dl.google.com/go/go%v.%v-%v.tar.gz", v, runtime.GOOS, runtime.GOARCH) 272 downloadArchive(check, goDir, url, "go", "") // skip SHA256 check as we fetch over https from a trusted domain 273 } 274 registerBinary("go"+v, filepath.Join(goDir, "bin", "go")) 275 finishWork() 276 } 277 registerBinary("go", filepath.Join(testDir, "go"+golangLatest, "bin", "go")) 278 registerBinary("gofmt", filepath.Join(testDir, "go"+golangLatest, "bin", "gofmt")) 279 280 // Download the staticcheck tool. 281 checkDir := startWork("staticcheck-" + staticcheckVersion) 282 if _, err := os.Stat(checkDir); err != nil { 283 fmt.Printf("download %v\n", filepath.Base(checkDir)) 284 url := fmt.Sprintf("https://github.com/dominikh/go-tools/releases/download/%v/staticcheck_%v_%v.tar.gz", staticcheckVersion, runtime.GOOS, runtime.GOARCH) 285 downloadArchive(check, checkDir, url, "staticcheck", staticcheckSHA256s[runtime.GOOS+"/"+runtime.GOARCH]) 286 } 287 registerBinary("staticcheck", filepath.Join(checkDir, "staticcheck")) 288 finishWork() 289 290 // GitHub actions sets GOROOT, which confuses invocations of the Go toolchain. 291 // Explicitly clear GOROOT, so each toolchain uses their default GOROOT. 292 check(os.Unsetenv("GOROOT")) 293 294 // Set a cache directory outside the test directory. 295 check(os.Setenv("GOCACHE", filepath.Join(repoRoot, ".gocache"))) 296 297 // Setup GOPATH for pre-module support (i.e., go1.10 and earlier). 298 goPath = startWork("gopath") 299 modulePath = strings.TrimSpace(command{Dir: testDir}.mustRun(t, "go", "list", "-m", "-f", "{{.Path}}")) 300 check(os.RemoveAll(filepath.Join(goPath, "src"))) 301 check(os.MkdirAll(filepath.Join(goPath, "src", filepath.Dir(modulePath)), 0775)) 302 check(os.Symlink(repoRoot, filepath.Join(goPath, "src", modulePath))) 303 command{Dir: repoRoot}.mustRun(t, "go", "mod", "tidy") 304 command{Dir: repoRoot}.mustRun(t, "go", "mod", "vendor") 305 check(os.Setenv("GOPATH", goPath)) 306 finishWork() 307 } 308 309 func downloadFile(check func(error), dstPath, srcURL string) { 310 resp, err := http.Get(srcURL) 311 check(err) 312 defer resp.Body.Close() 313 314 check(os.MkdirAll(filepath.Dir(dstPath), 0775)) 315 f, err := os.Create(dstPath) 316 check(err) 317 318 _, err = io.Copy(f, resp.Body) 319 check(err) 320 } 321 322 func downloadArchive(check func(error), dstPath, srcURL, skipPrefix, wantSHA256 string) { 323 check(os.RemoveAll(dstPath)) 324 325 resp, err := http.Get(srcURL) 326 check(err) 327 defer resp.Body.Close() 328 329 var r io.Reader = resp.Body 330 if wantSHA256 != "" { 331 b, err := ioutil.ReadAll(resp.Body) 332 check(err) 333 r = bytes.NewReader(b) 334 335 if gotSHA256 := fmt.Sprintf("%x", sha256.Sum256(b)); gotSHA256 != wantSHA256 { 336 check(fmt.Errorf("checksum validation error:\ngot %v\nwant %v", gotSHA256, wantSHA256)) 337 } 338 } 339 340 zr, err := gzip.NewReader(r) 341 check(err) 342 343 tr := tar.NewReader(zr) 344 for { 345 h, err := tr.Next() 346 if err == io.EOF { 347 return 348 } 349 check(err) 350 351 // Skip directories or files outside the prefix directory. 352 if len(skipPrefix) > 0 { 353 if !strings.HasPrefix(h.Name, skipPrefix) { 354 continue 355 } 356 if len(h.Name) > len(skipPrefix) && h.Name[len(skipPrefix)] != '/' { 357 continue 358 } 359 } 360 361 path := strings.TrimPrefix(strings.TrimPrefix(h.Name, skipPrefix), "/") 362 path = filepath.Join(dstPath, filepath.FromSlash(path)) 363 mode := os.FileMode(h.Mode & 0777) 364 switch h.Typeflag { 365 case tar.TypeReg: 366 b, err := ioutil.ReadAll(tr) 367 check(err) 368 check(ioutil.WriteFile(path, b, mode)) 369 case tar.TypeDir: 370 check(os.Mkdir(path, mode)) 371 } 372 } 373 } 374 375 func mustHandleFlags(t *testing.T) { 376 if *regenerate { 377 t.Run("Generate", func(t *testing.T) { 378 fmt.Print(mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-types", "-execute")) 379 fmt.Print(mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-protos", "-execute")) 380 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n") 381 mustRunCommand(t, append([]string{"gofmt", "-w"}, files...)...) 382 }) 383 } 384 if *buildRelease { 385 t.Run("BuildRelease", func(t *testing.T) { 386 v := version.String() 387 for _, goos := range []string{"linux", "darwin", "windows"} { 388 for _, goarch := range []string{"386", "amd64", "arm64"} { 389 // Avoid Darwin since 10.15 dropped support for i386. 390 if goos == "darwin" && goarch == "386" { 391 continue 392 } 393 394 binPath := filepath.Join("bin", fmt.Sprintf("protoc-gen-go.%v.%v.%v", v, goos, goarch)) 395 396 // Build the binary. 397 cmd := command{Env: append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)} 398 cmd.mustRun(t, "go", "build", "-trimpath", "-ldflags", "-s -w -buildid=", "-o", binPath, "./cmd/protoc-gen-go") 399 400 // Archive and compress the binary. 401 in, err := ioutil.ReadFile(binPath) 402 if err != nil { 403 t.Fatal(err) 404 } 405 out := new(bytes.Buffer) 406 suffix := "" 407 comment := fmt.Sprintf("protoc-gen-go VERSION=%v GOOS=%v GOARCH=%v", v, goos, goarch) 408 switch goos { 409 case "windows": 410 suffix = ".zip" 411 zw := zip.NewWriter(out) 412 zw.SetComment(comment) 413 fw, _ := zw.Create("protoc-gen-go.exe") 414 fw.Write(in) 415 zw.Close() 416 default: 417 suffix = ".tar.gz" 418 gz, _ := gzip.NewWriterLevel(out, gzip.BestCompression) 419 gz.Comment = comment 420 tw := tar.NewWriter(gz) 421 tw.WriteHeader(&tar.Header{ 422 Name: "protoc-gen-go", 423 Mode: int64(0775), 424 Size: int64(len(in)), 425 }) 426 tw.Write(in) 427 tw.Close() 428 gz.Close() 429 } 430 if err := ioutil.WriteFile(binPath+suffix, out.Bytes(), 0664); err != nil { 431 t.Fatal(err) 432 } 433 } 434 } 435 }) 436 } 437 if *regenerate || *buildRelease { 438 t.SkipNow() 439 } 440 } 441 442 var copyrightRegex = []*regexp.Regexp{ 443 regexp.MustCompile(`^// Copyright \d\d\d\d The Go Authors\. All rights reserved. 444 // Use of this source code is governed by a BSD-style 445 // license that can be found in the LICENSE file\. 446 `), 447 // Generated .pb.go files from main protobuf repo. 448 regexp.MustCompile(`^// Protocol Buffers - Google's data interchange format 449 // Copyright \d\d\d\d Google Inc\. All rights reserved\. 450 `), 451 } 452 453 func mustHaveCopyrightHeader(t *testing.T, files []string) { 454 var bad []string 455 File: 456 for _, file := range files { 457 b, err := ioutil.ReadFile(file) 458 if err != nil { 459 t.Fatal(err) 460 } 461 for _, re := range copyrightRegex { 462 if loc := re.FindIndex(b); loc != nil && loc[0] == 0 { 463 continue File 464 } 465 } 466 bad = append(bad, file) 467 } 468 if len(bad) > 0 { 469 t.Fatalf("files with missing/bad copyright headers:\n %v", strings.Join(bad, "\n ")) 470 } 471 } 472 473 type command struct { 474 Dir string 475 Env []string 476 } 477 478 func (c command) mustRun(t *testing.T, args ...string) string { 479 t.Helper() 480 stdout := new(bytes.Buffer) 481 stderr := new(bytes.Buffer) 482 cmd := exec.Command(args[0], args[1:]...) 483 cmd.Dir = "." 484 if c.Dir != "" { 485 cmd.Dir = c.Dir 486 } 487 cmd.Env = os.Environ() 488 if c.Env != nil { 489 cmd.Env = c.Env 490 } 491 cmd.Env = append(cmd.Env, "PWD="+cmd.Dir) 492 cmd.Stdout = stdout 493 cmd.Stderr = stderr 494 if err := cmd.Run(); err != nil { 495 t.Fatalf("executing (%v): %v\n%s%s", strings.Join(args, " "), err, stdout.String(), stderr.String()) 496 } 497 return stdout.String() 498 } 499 500 func mustRunCommand(t *testing.T, args ...string) string { 501 t.Helper() 502 return command{}.mustRun(t, args...) 503 }