1
2
3
4
5 package main
6
7 import (
8 "archive/tar"
9 "archive/zip"
10 "bytes"
11 "compress/gzip"
12 "crypto/sha256"
13 "flag"
14 "fmt"
15 "io"
16 "io/fs"
17 "net/http"
18 "os"
19 "os/exec"
20 "path/filepath"
21 "reflect"
22 "regexp"
23 "runtime"
24 "runtime/debug"
25 "strings"
26 "sync"
27 "testing"
28 "time"
29
30 "google.golang.org/protobuf/internal/version"
31 )
32
33 var (
34 regenerate = flag.Bool("regenerate", false, "regenerate files")
35 buildRelease = flag.Bool("buildRelease", false, "build release binaries")
36
37 protobufVersion = "27.0-rc1"
38
39 golangVersions = func() []string {
40
41 return []string{
42 "1.17.13",
43 "1.18.10",
44 "1.19.13",
45 "1.20.12",
46 "1.21.5",
47 }
48 }()
49 golangLatest = golangVersions[len(golangVersions)-1]
50
51 staticcheckVersion = "2023.1.6"
52 staticcheckSHA256s = map[string]string{
53 "darwin/amd64": "b14a0cbd3c238713f5f9db41550893ea7d75d8d7822491c7f4e33e2fe43f6305",
54 "darwin/arm64": "f1c869abe6be2c6ab727dc9d6049766c947534766d71a1798c12a37526ea2b6f",
55 "linux/386": "02859a7c44c7b5ab41a70d9b8107c01ab8d2c94075bae3d0b02157aff743ca42",
56 "linux/amd64": "45337834da5dc7b8eff01cb6b3837e3759503cfbb8edf36b09e42f32bccb1f6e",
57 }
58
59
60 purgeTimeout = 30 * 24 * time.Hour
61
62
63 modulePath string
64 protobufPath string
65 )
66
67 func TestIntegration(t *testing.T) {
68 if testing.Short() {
69 t.Skip("skipping integration test in short mode")
70 }
71 if os.Getenv("GO_BUILDER_NAME") != "" {
72
73 if race() {
74 t.Skip("skipping integration test in race mode on builders")
75 }
76
77
78 if os.Getenv("GO_PROTOBUF_INTEGRATION_TEST_RUNNING") == "1" {
79 t.Skip("protobuf integration test is already running, skipping nested invocation")
80 }
81 os.Setenv("GO_PROTOBUF_INTEGRATION_TEST_RUNNING", "1")
82 } else if flag.Lookup("test.run").Value.String() != "^TestIntegration$" {
83 t.Skip("not running integration test if not explicitly requested via test.bash")
84 }
85
86 mustInitDeps(t)
87 mustHandleFlags(t)
88
89
90
91
92
93 gitDiff := mustRunCommand(t, "git", "diff", "HEAD")
94 if strings.TrimSpace(gitDiff) != "" {
95 fmt.Printf("WARNING: working tree contains uncommitted changes:\n%v\n", gitDiff)
96 }
97 gitUntracked := mustRunCommand(t, "git", "ls-files", "--others", "--exclude-standard")
98 if strings.TrimSpace(gitUntracked) != "" {
99 fmt.Printf("WARNING: working tree contains untracked files:\n%v\n", gitUntracked)
100 }
101
102
103 t.Run("GeneratedGoFiles", func(t *testing.T) {
104 diff := mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-types")
105 if strings.TrimSpace(diff) != "" {
106 t.Fatalf("stale generated files:\n%v", diff)
107 }
108 diff = mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-protos")
109 if strings.TrimSpace(diff) != "" {
110 t.Fatalf("stale generated files:\n%v", diff)
111 }
112 })
113 t.Run("FormattedGoFiles", func(t *testing.T) {
114 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n")
115 diff := mustRunCommand(t, append([]string{"gofmt", "-d"}, files...)...)
116 if strings.TrimSpace(diff) != "" {
117 t.Fatalf("unformatted source files:\n%v", diff)
118 }
119 })
120 t.Run("CopyrightHeaders", func(t *testing.T) {
121 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go", "*.proto")), "\n")
122 mustHaveCopyrightHeader(t, files)
123 })
124
125 var wg sync.WaitGroup
126 sema := make(chan bool, (runtime.NumCPU()+1)/2)
127 for i := range golangVersions {
128 goVersion := golangVersions[i]
129 goLabel := "Go" + goVersion
130 runGo := func(label string, cmd command, args ...string) {
131 wg.Add(1)
132 sema <- true
133 go func() {
134 defer wg.Done()
135 defer func() { <-sema }()
136 t.Run(goLabel+"/"+label, func(t *testing.T) {
137 args[0] += goVersion
138 cmd.mustRun(t, args...)
139 })
140 }()
141 }
142
143 runGo("Normal", command{}, "go", "test", "-race", "./...")
144 runGo("PureGo", command{}, "go", "test", "-race", "-tags", "purego", "./...")
145 runGo("Reflect", command{}, "go", "test", "-race", "-tags", "protoreflect", "./...")
146 if goVersion == golangLatest {
147 runGo("ProtoLegacyRace", command{}, "go", "test", "-race", "-tags", "protolegacy", "./...")
148 runGo("ProtoLegacy", command{}, "go", "test", "-tags", "protolegacy", "./...")
149 runGo("ProtocGenGo", command{Dir: "cmd/protoc-gen-go/testdata"}, "go", "test")
150 runGo("Conformance", command{Dir: "internal/conformance"}, "go", "test", "-execute")
151
152
153
154 if runtime.GOOS == "linux" {
155 runGo("Arch32Bit", command{Env: append(os.Environ(), "GOARCH=386")}, "go", "test", "./...")
156 }
157 }
158 }
159 wg.Wait()
160
161 t.Run("GoStaticCheck", func(t *testing.T) {
162 checks := []string{
163 "all",
164 "-SA1019",
165 "-S*",
166 "-ST*",
167 "-U*",
168 }
169 out := mustRunCommand(t, "staticcheck", "-checks="+strings.Join(checks, ","), "-fail=none", "./...")
170
171
172 var findings []string
173 for _, finding := range strings.Split(strings.TrimSpace(out), "\n") {
174 switch {
175 case strings.HasPrefix(finding, "internal/testprotos/legacy/"):
176 default:
177 findings = append(findings, finding)
178 }
179 }
180 if len(findings) > 0 {
181 t.Fatalf("staticcheck findings:\n%v", strings.Join(findings, "\n"))
182 }
183 })
184 t.Run("CommittedGitChanges", func(t *testing.T) {
185 if strings.TrimSpace(gitDiff) != "" {
186 t.Fatalf("uncommitted changes")
187 }
188 })
189 t.Run("TrackedGitFiles", func(t *testing.T) {
190 if strings.TrimSpace(gitUntracked) != "" {
191 t.Fatalf("untracked files")
192 }
193 })
194 }
195
196 func mustInitDeps(t *testing.T) {
197 check := func(err error) {
198 t.Helper()
199 if err != nil {
200 t.Fatal(err)
201 }
202 }
203
204
205 repoRoot, err := os.Getwd()
206 check(err)
207 testDir := filepath.Join(repoRoot, ".cache")
208 check(os.MkdirAll(testDir, 0775))
209
210
211
212 var workingDir string
213 finishedDirs := map[string]bool{}
214 defer func() {
215 if workingDir != "" {
216 os.RemoveAll(workingDir)
217 }
218 }()
219 startWork := func(name string) string {
220 workingDir = filepath.Join(testDir, name)
221 return workingDir
222 }
223 finishWork := func() {
224 finishedDirs[workingDir] = true
225 workingDir = ""
226 }
227
228
229 defer func() {
230 now := time.Now()
231 fis, _ := os.ReadDir(testDir)
232 for _, fi := range fis {
233 dir := filepath.Join(testDir, fi.Name())
234 if finishedDirs[dir] {
235 os.Chtimes(dir, now, now)
236 continue
237 }
238 fii, err := fi.Info()
239 check(err)
240 if now.Sub(fii.ModTime()) < purgeTimeout {
241 continue
242 }
243 fmt.Printf("delete %v\n", fi.Name())
244 os.RemoveAll(dir)
245 }
246 }()
247
248
249
250 binPath := startWork("bin")
251 check(os.RemoveAll(binPath))
252 check(os.Mkdir(binPath, 0775))
253 check(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))
254 registerBinary := func(name, path string) {
255 check(os.Symlink(path, filepath.Join(binPath, name)))
256 }
257 finishWork()
258
259
260 protobufPath = startWork("protobuf-" + protobufVersion)
261 if _, err := os.Stat(protobufPath); err != nil {
262 fmt.Printf("download %v\n", filepath.Base(protobufPath))
263 checkoutVersion := protobufVersion
264 if isCommit := strings.Trim(protobufVersion, "0123456789abcdef") == ""; !isCommit {
265
266 checkoutVersion = "v" + protobufVersion
267 }
268 command{Dir: testDir}.mustRun(t, "git", "clone", "https://github.com/protocolbuffers/protobuf", "protobuf-"+protobufVersion)
269 command{Dir: protobufPath}.mustRun(t, "git", "checkout", checkoutVersion)
270
271 if os.Getenv("GO_BUILDER_NAME") != "" {
272
273
274
275 protocPath, err := exec.LookPath("protoc")
276 check(err)
277 confTestRunnerPath, err := exec.LookPath("conformance_test_runner")
278 check(err)
279 check(os.MkdirAll(filepath.Join(protobufPath, "bazel-bin", "conformance"), 0775))
280 check(os.Symlink(protocPath, filepath.Join(protobufPath, "bazel-bin", "protoc")))
281 check(os.Symlink(confTestRunnerPath, filepath.Join(protobufPath, "bazel-bin", "conformance", "conformance_test_runner")))
282 } else {
283
284
285
286 fmt.Printf("build %v\n", filepath.Base(protobufPath))
287 env := os.Environ()
288 args := []string{
289 "bazel", "build",
290 ":protoc",
291 "//conformance:conformance_test_runner",
292 }
293 if runtime.GOOS == "darwin" {
294
295 env = append(env, "CC=clang")
296
297 args = append(args,
298 "--macos_minimum_os=13.0",
299 "--host_macos_minimum_os=13.0",
300 )
301 }
302 command{
303 Dir: protobufPath,
304 Env: env,
305 }.mustRun(t, args...)
306 }
307 }
308 check(os.Setenv("PROTOBUF_ROOT", protobufPath))
309 registerBinary("conform-test-runner", filepath.Join(protobufPath, "bazel-bin", "conformance", "conformance_test_runner"))
310 registerBinary("protoc", filepath.Join(protobufPath, "bazel-bin", "protoc"))
311 finishWork()
312
313
314 for _, v := range golangVersions {
315 goDir := startWork("go" + v)
316 if _, err := os.Stat(goDir); err != nil {
317 fmt.Printf("download %v\n", filepath.Base(goDir))
318 url := fmt.Sprintf("https://dl.google.com/go/go%v.%v-%v.tar.gz", v, runtime.GOOS, runtime.GOARCH)
319 downloadArchive(check, goDir, url, "go", "")
320 }
321 registerBinary("go"+v, filepath.Join(goDir, "bin", "go"))
322 finishWork()
323 }
324 registerBinary("go", filepath.Join(testDir, "go"+golangLatest, "bin", "go"))
325 registerBinary("gofmt", filepath.Join(testDir, "go"+golangLatest, "bin", "gofmt"))
326
327
328 checkDir := startWork("staticcheck-" + staticcheckVersion)
329 if _, err := os.Stat(checkDir); err != nil {
330 fmt.Printf("download %v\n", filepath.Base(checkDir))
331 url := fmt.Sprintf("https://github.com/dominikh/go-tools/releases/download/%v/staticcheck_%v_%v.tar.gz", staticcheckVersion, runtime.GOOS, runtime.GOARCH)
332 downloadArchive(check, checkDir, url, "staticcheck", staticcheckSHA256s[runtime.GOOS+"/"+runtime.GOARCH])
333 }
334 registerBinary("staticcheck", filepath.Join(checkDir, "staticcheck"))
335 finishWork()
336
337
338
339 check(os.Unsetenv("GOROOT"))
340
341
342 check(os.Setenv("GOCACHE", filepath.Join(repoRoot, ".gocache")))
343 }
344
345 func downloadFile(check func(error), dstPath, srcURL string, perm fs.FileMode) {
346 resp, err := http.Get(srcURL)
347 check(err)
348 defer resp.Body.Close()
349 if resp.StatusCode != http.StatusOK {
350 body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
351 check(fmt.Errorf("GET %q: non-200 OK status code: %v body: %q", srcURL, resp.Status, body))
352 }
353
354 check(os.MkdirAll(filepath.Dir(dstPath), 0775))
355 f, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
356 check(err)
357
358 _, err = io.Copy(f, resp.Body)
359 check(err)
360
361 check(f.Close())
362 }
363
364 func downloadArchive(check func(error), dstPath, srcURL, skipPrefix, wantSHA256 string) {
365 check(os.RemoveAll(dstPath))
366
367 resp, err := http.Get(srcURL)
368 check(err)
369 defer resp.Body.Close()
370 if resp.StatusCode != http.StatusOK {
371 body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
372 check(fmt.Errorf("GET %q: non-200 OK status code: %v body: %q", srcURL, resp.Status, body))
373 }
374
375 var r io.Reader = resp.Body
376 if wantSHA256 != "" {
377 b, err := io.ReadAll(resp.Body)
378 check(err)
379 r = bytes.NewReader(b)
380
381 if gotSHA256 := fmt.Sprintf("%x", sha256.Sum256(b)); gotSHA256 != wantSHA256 {
382 check(fmt.Errorf("checksum validation error:\ngot %v\nwant %v", gotSHA256, wantSHA256))
383 }
384 }
385
386 zr, err := gzip.NewReader(r)
387 check(err)
388
389 tr := tar.NewReader(zr)
390 for {
391 h, err := tr.Next()
392 if err == io.EOF {
393 return
394 }
395 check(err)
396
397
398 if len(skipPrefix) > 0 {
399 if !strings.HasPrefix(h.Name, skipPrefix) {
400 continue
401 }
402 if len(h.Name) > len(skipPrefix) && h.Name[len(skipPrefix)] != '/' {
403 continue
404 }
405 }
406
407 path := strings.TrimPrefix(strings.TrimPrefix(h.Name, skipPrefix), "/")
408 path = filepath.Join(dstPath, filepath.FromSlash(path))
409 mode := os.FileMode(h.Mode & 0777)
410 switch h.Typeflag {
411 case tar.TypeReg:
412 b, err := io.ReadAll(tr)
413 check(err)
414 check(os.WriteFile(path, b, mode))
415 case tar.TypeDir:
416 check(os.Mkdir(path, mode))
417 }
418 }
419 }
420
421 func mustHandleFlags(t *testing.T) {
422 if *regenerate {
423 t.Run("Generate", func(t *testing.T) {
424 fmt.Print(mustRunCommand(t, "go", "generate", "./internal/cmd/generate-types"))
425 fmt.Print(mustRunCommand(t, "go", "generate", "./internal/cmd/generate-protos"))
426 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n")
427 mustRunCommand(t, append([]string{"gofmt", "-w"}, files...)...)
428 })
429 }
430 if *buildRelease {
431 t.Run("BuildRelease", func(t *testing.T) {
432 v := version.String()
433 for _, goos := range []string{"linux", "darwin", "windows"} {
434 for _, goarch := range []string{"386", "amd64", "arm64"} {
435
436 if goos == "darwin" && goarch == "386" {
437 continue
438 }
439
440 binPath := filepath.Join("bin", fmt.Sprintf("protoc-gen-go.%v.%v.%v", v, goos, goarch))
441
442
443 cmd := command{Env: append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)}
444 cmd.mustRun(t, "go", "build", "-trimpath", "-ldflags", "-s -w -buildid=", "-o", binPath, "./cmd/protoc-gen-go")
445
446
447 in, err := os.ReadFile(binPath)
448 if err != nil {
449 t.Fatal(err)
450 }
451 out := new(bytes.Buffer)
452 suffix := ""
453 comment := fmt.Sprintf("protoc-gen-go VERSION=%v GOOS=%v GOARCH=%v", v, goos, goarch)
454 switch goos {
455 case "windows":
456 suffix = ".zip"
457 zw := zip.NewWriter(out)
458 zw.SetComment(comment)
459 fw, _ := zw.Create("protoc-gen-go.exe")
460 fw.Write(in)
461 zw.Close()
462 default:
463 suffix = ".tar.gz"
464 gz, _ := gzip.NewWriterLevel(out, gzip.BestCompression)
465 gz.Comment = comment
466 tw := tar.NewWriter(gz)
467 tw.WriteHeader(&tar.Header{
468 Name: "protoc-gen-go",
469 Mode: int64(0775),
470 Size: int64(len(in)),
471 })
472 tw.Write(in)
473 tw.Close()
474 gz.Close()
475 }
476 if err := os.WriteFile(binPath+suffix, out.Bytes(), 0664); err != nil {
477 t.Fatal(err)
478 }
479 }
480 }
481 })
482 }
483 if *regenerate || *buildRelease {
484 t.SkipNow()
485 }
486 }
487
488 var copyrightRegex = []*regexp.Regexp{
489 regexp.MustCompile(`^// Copyright \d\d\d\d The Go Authors\. All rights reserved.
490 // Use of this source code is governed by a BSD-style
491 // license that can be found in the LICENSE file\.
492 `),
493
494 regexp.MustCompile(`^// Protocol Buffers - Google's data interchange format
495 // Copyright \d\d\d\d Google Inc\. All rights reserved\.
496 `),
497 }
498
499 func mustHaveCopyrightHeader(t *testing.T, files []string) {
500 var bad []string
501 File:
502 for _, file := range files {
503 if strings.HasSuffix(file, "internal/testprotos/conformance/editions/test_messages_edition2023.pb.go") {
504
505
506
507 continue
508 }
509 b, err := os.ReadFile(file)
510 if err != nil {
511 t.Fatal(err)
512 }
513 for _, re := range copyrightRegex {
514 if loc := re.FindIndex(b); loc != nil && loc[0] == 0 {
515 continue File
516 }
517 }
518 bad = append(bad, file)
519 }
520 if len(bad) > 0 {
521 t.Fatalf("files with missing/bad copyright headers:\n %v", strings.Join(bad, "\n "))
522 }
523 }
524
525 type command struct {
526 Dir string
527 Env []string
528 }
529
530 func (c command) mustRun(t *testing.T, args ...string) string {
531 t.Helper()
532 stdout := new(bytes.Buffer)
533 stderr := new(bytes.Buffer)
534 cmd := exec.Command(args[0], args[1:]...)
535 cmd.Dir = "."
536 if c.Dir != "" {
537 cmd.Dir = c.Dir
538 }
539 cmd.Env = os.Environ()
540 if c.Env != nil {
541 cmd.Env = c.Env
542 }
543 cmd.Env = append(cmd.Env, "PWD="+cmd.Dir)
544 cmd.Stdout = stdout
545 cmd.Stderr = stderr
546 if err := cmd.Run(); err != nil {
547 t.Fatalf("executing (%v): %v\n%s%s", strings.Join(args, " "), err, stdout.String(), stderr.String())
548 }
549 return stdout.String()
550 }
551
552 func mustRunCommand(t *testing.T, args ...string) string {
553 t.Helper()
554 return command{}.mustRun(t, args...)
555 }
556
557
558
559
560
561 func race() bool {
562 bi, ok := debug.ReadBuildInfo()
563 if !ok {
564 return false
565 }
566
567
568 s := reflect.ValueOf(bi).Elem().FieldByName("Settings")
569 if !s.IsValid() {
570 return false
571 }
572 for i := 0; i < s.Len(); i++ {
573 if s.Index(i).FieldByName("Key").String() == "-race" {
574 return s.Index(i).FieldByName("Value").String() == "true"
575 }
576 }
577 return false
578 }
579
View as plain text