1package main 2 3import ( 4 "bytes" 5 "flag" 6 "fmt" 7 "go/build" 8 "go/parser" 9 "go/token" 10 "io/ioutil" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "regexp" 15 "runtime" 16 "strings" 17 "testing" 18) 19 20// Set --regenerate to regenerate the golden files. 21var regenerate = flag.Bool("regenerate", false, "regenerate golden files") 22 23// When the environment variable RUN_AS_PROTOC_GEN_GO is set, we skip running 24// tests and instead act as protoc-gen-go. This allows the test binary to 25// pass itself to protoc. 26func init() { 27 if os.Getenv("RUN_AS_PROTOC_GEN_GO") != "" { 28 main() 29 os.Exit(0) 30 } 31} 32 33func TestGolden(t *testing.T) { 34 workdir, err := ioutil.TempDir("", "proto-test") 35 if err != nil { 36 t.Fatal(err) 37 } 38 defer os.RemoveAll(workdir) 39 40 // Find all the proto files we need to compile. We assume that each directory 41 // contains the files for a single package. 42 supportTypeAliases := hasReleaseTag("go1.9") 43 packages := map[string][]string{} 44 err = filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { 45 if filepath.Base(path) == "import_public" && !supportTypeAliases { 46 // Public imports require type alias support. 47 return filepath.SkipDir 48 } 49 if !strings.HasSuffix(path, ".proto") { 50 return nil 51 } 52 dir := filepath.Dir(path) 53 packages[dir] = append(packages[dir], path) 54 return nil 55 }) 56 if err != nil { 57 t.Fatal(err) 58 } 59 60 // Compile each package, using this binary as protoc-gen-go. 61 for _, sources := range packages { 62 args := []string{"-Itestdata", "--go_out=plugins=grpc,paths=source_relative:" + workdir} 63 args = append(args, sources...) 64 protoc(t, args) 65 } 66 67 // Compare each generated file to the golden version. 68 filepath.Walk(workdir, func(genPath string, info os.FileInfo, _ error) error { 69 if info.IsDir() { 70 return nil 71 } 72 73 // For each generated file, figure out the path to the corresponding 74 // golden file in the testdata directory. 75 relPath, err := filepath.Rel(workdir, genPath) 76 if err != nil { 77 t.Errorf("filepath.Rel(%q, %q): %v", workdir, genPath, err) 78 return nil 79 } 80 if filepath.SplitList(relPath)[0] == ".." { 81 t.Errorf("generated file %q is not relative to %q", genPath, workdir) 82 } 83 goldenPath := filepath.Join("testdata", relPath) 84 85 got, err := ioutil.ReadFile(genPath) 86 if err != nil { 87 t.Error(err) 88 return nil 89 } 90 if *regenerate { 91 // If --regenerate set, just rewrite the golden files. 92 err := ioutil.WriteFile(goldenPath, got, 0666) 93 if err != nil { 94 t.Error(err) 95 } 96 return nil 97 } 98 99 want, err := ioutil.ReadFile(goldenPath) 100 if err != nil { 101 t.Error(err) 102 return nil 103 } 104 105 want = fdescRE.ReplaceAll(want, nil) 106 got = fdescRE.ReplaceAll(got, nil) 107 if bytes.Equal(got, want) { 108 return nil 109 } 110 111 cmd := exec.Command("diff", "-u", goldenPath, genPath) 112 out, _ := cmd.CombinedOutput() 113 t.Errorf("golden file differs: %v\n%v", relPath, string(out)) 114 return nil 115 }) 116} 117 118var fdescRE = regexp.MustCompile(`(?ms)^var fileDescriptor.*}`) 119 120// Source files used by TestParameters. 121const ( 122 aProto = ` 123syntax = "proto3"; 124package test.alpha; 125option go_package = "package/alpha"; 126import "beta/b.proto"; 127message M { test.beta.M field = 1; }` 128 129 bProto = ` 130syntax = "proto3"; 131package test.beta; 132// no go_package option 133message M {}` 134) 135 136func TestParameters(t *testing.T) { 137 for _, test := range []struct { 138 parameters string 139 wantFiles map[string]bool 140 wantImportsA map[string]bool 141 wantPackageA string 142 wantPackageB string 143 }{{ 144 parameters: "", 145 wantFiles: map[string]bool{ 146 "package/alpha/a.pb.go": true, 147 "beta/b.pb.go": true, 148 }, 149 wantPackageA: "alpha", 150 wantPackageB: "test_beta", 151 wantImportsA: map[string]bool{ 152 "github.com/golang/protobuf/proto": true, 153 "beta": true, 154 }, 155 }, { 156 parameters: "import_prefix=prefix", 157 wantFiles: map[string]bool{ 158 "package/alpha/a.pb.go": true, 159 "beta/b.pb.go": true, 160 }, 161 wantPackageA: "alpha", 162 wantPackageB: "test_beta", 163 wantImportsA: map[string]bool{ 164 // This really doesn't seem like useful behavior. 165 "prefixgithub.com/golang/protobuf/proto": true, 166 "prefixbeta": true, 167 }, 168 }, { 169 // import_path only affects the 'package' line. 170 parameters: "import_path=import/path/of/pkg", 171 wantPackageA: "alpha", 172 wantPackageB: "pkg", 173 wantFiles: map[string]bool{ 174 "package/alpha/a.pb.go": true, 175 "beta/b.pb.go": true, 176 }, 177 }, { 178 parameters: "Mbeta/b.proto=package/gamma", 179 wantFiles: map[string]bool{ 180 "package/alpha/a.pb.go": true, 181 "beta/b.pb.go": true, 182 }, 183 wantPackageA: "alpha", 184 wantPackageB: "test_beta", 185 wantImportsA: map[string]bool{ 186 "github.com/golang/protobuf/proto": true, 187 // Rewritten by the M parameter. 188 "package/gamma": true, 189 }, 190 }, { 191 parameters: "import_prefix=prefix,Mbeta/b.proto=package/gamma", 192 wantFiles: map[string]bool{ 193 "package/alpha/a.pb.go": true, 194 "beta/b.pb.go": true, 195 }, 196 wantPackageA: "alpha", 197 wantPackageB: "test_beta", 198 wantImportsA: map[string]bool{ 199 // import_prefix applies after M. 200 "prefixpackage/gamma": true, 201 }, 202 }, { 203 parameters: "paths=source_relative", 204 wantFiles: map[string]bool{ 205 "alpha/a.pb.go": true, 206 "beta/b.pb.go": true, 207 }, 208 wantPackageA: "alpha", 209 wantPackageB: "test_beta", 210 }, { 211 parameters: "paths=source_relative,import_prefix=prefix", 212 wantFiles: map[string]bool{ 213 // import_prefix doesn't affect filenames. 214 "alpha/a.pb.go": true, 215 "beta/b.pb.go": true, 216 }, 217 wantPackageA: "alpha", 218 wantPackageB: "test_beta", 219 }} { 220 name := test.parameters 221 if name == "" { 222 name = "defaults" 223 } 224 // TODO: Switch to t.Run when we no longer support Go 1.6. 225 t.Logf("TEST: %v", name) 226 workdir, err := ioutil.TempDir("", "proto-test") 227 if err != nil { 228 t.Fatal(err) 229 } 230 defer os.RemoveAll(workdir) 231 232 for _, dir := range []string{"alpha", "beta", "out"} { 233 if err := os.MkdirAll(filepath.Join(workdir, dir), 0777); err != nil { 234 t.Fatal(err) 235 } 236 } 237 238 if err := ioutil.WriteFile(filepath.Join(workdir, "alpha", "a.proto"), []byte(aProto), 0666); err != nil { 239 t.Fatal(err) 240 } 241 242 if err := ioutil.WriteFile(filepath.Join(workdir, "beta", "b.proto"), []byte(bProto), 0666); err != nil { 243 t.Fatal(err) 244 } 245 246 protoc(t, []string{ 247 "-I" + workdir, 248 "--go_out=" + test.parameters + ":" + filepath.Join(workdir, "out"), 249 filepath.Join(workdir, "alpha", "a.proto"), 250 }) 251 protoc(t, []string{ 252 "-I" + workdir, 253 "--go_out=" + test.parameters + ":" + filepath.Join(workdir, "out"), 254 filepath.Join(workdir, "beta", "b.proto"), 255 }) 256 257 contents := make(map[string]string) 258 gotFiles := make(map[string]bool) 259 outdir := filepath.Join(workdir, "out") 260 filepath.Walk(outdir, func(p string, info os.FileInfo, _ error) error { 261 if info.IsDir() { 262 return nil 263 } 264 base := filepath.Base(p) 265 if base == "a.pb.go" || base == "b.pb.go" { 266 b, err := ioutil.ReadFile(p) 267 if err != nil { 268 t.Fatal(err) 269 } 270 contents[base] = string(b) 271 } 272 relPath, _ := filepath.Rel(outdir, p) 273 gotFiles[relPath] = true 274 return nil 275 }) 276 for got := range gotFiles { 277 if runtime.GOOS == "windows" { 278 got = filepath.ToSlash(got) 279 } 280 if !test.wantFiles[got] { 281 t.Errorf("unexpected output file: %v", got) 282 } 283 } 284 for want := range test.wantFiles { 285 if runtime.GOOS == "windows" { 286 want = filepath.FromSlash(want) 287 } 288 if !gotFiles[want] { 289 t.Errorf("missing output file: %v", want) 290 } 291 } 292 gotPackageA, gotImports, err := parseFile(contents["a.pb.go"]) 293 if err != nil { 294 t.Fatal(err) 295 } 296 gotPackageB, _, err := parseFile(contents["b.pb.go"]) 297 if err != nil { 298 t.Fatal(err) 299 } 300 if got, want := gotPackageA, test.wantPackageA; want != got { 301 t.Errorf("output file a.pb.go is package %q, want %q", got, want) 302 } 303 if got, want := gotPackageB, test.wantPackageB; want != got { 304 t.Errorf("output file b.pb.go is package %q, want %q", got, want) 305 } 306 missingImport := false 307 WantImport: 308 for want := range test.wantImportsA { 309 for _, imp := range gotImports { 310 if `"`+want+`"` == imp { 311 continue WantImport 312 } 313 } 314 t.Errorf("output file a.pb.go does not contain expected import %q", want) 315 missingImport = true 316 } 317 if missingImport { 318 t.Error("got imports:") 319 for _, imp := range gotImports { 320 t.Errorf(" %v", imp) 321 } 322 } 323 } 324} 325 326func TestPackageComment(t *testing.T) { 327 workdir, err := ioutil.TempDir("", "proto-test") 328 if err != nil { 329 t.Fatal(err) 330 } 331 defer os.RemoveAll(workdir) 332 333 var packageRE = regexp.MustCompile(`(?m)^package .*`) 334 335 for i, test := range []struct { 336 goPackageOption string 337 wantPackage string 338 }{{ 339 goPackageOption: ``, 340 wantPackage: `package proto_package`, 341 }, { 342 goPackageOption: `option go_package = "go_package";`, 343 wantPackage: `package go_package`, 344 }, { 345 goPackageOption: `option go_package = "import/path/of/go_package";`, 346 wantPackage: `package go_package // import "import/path/of/go_package"`, 347 }, { 348 goPackageOption: `option go_package = "import/path/of/something;go_package";`, 349 wantPackage: `package go_package // import "import/path/of/something"`, 350 }, { 351 goPackageOption: `option go_package = "import_path;go_package";`, 352 wantPackage: `package go_package // import "import_path"`, 353 }} { 354 srcName := filepath.Join(workdir, fmt.Sprintf("%d.proto", i)) 355 tgtName := filepath.Join(workdir, fmt.Sprintf("%d.pb.go", i)) 356 357 buf := &bytes.Buffer{} 358 fmt.Fprintln(buf, `syntax = "proto3";`) 359 fmt.Fprintln(buf, `package proto_package;`) 360 fmt.Fprintln(buf, test.goPackageOption) 361 if err := ioutil.WriteFile(srcName, buf.Bytes(), 0666); err != nil { 362 t.Fatal(err) 363 } 364 365 protoc(t, []string{"-I" + workdir, "--go_out=paths=source_relative:" + workdir, srcName}) 366 367 out, err := ioutil.ReadFile(tgtName) 368 if err != nil { 369 t.Fatal(err) 370 } 371 372 pkg := packageRE.Find(out) 373 if pkg == nil { 374 t.Errorf("generated .pb.go contains no package line\n\nsource:\n%v\n\noutput:\n%v", buf.String(), string(out)) 375 continue 376 } 377 378 if got, want := string(pkg), test.wantPackage; got != want { 379 t.Errorf("unexpected package statement with go_package = %q\n got: %v\nwant: %v", test.goPackageOption, got, want) 380 } 381 } 382} 383 384// parseFile returns a file's package name and a list of all packages it imports. 385func parseFile(source string) (packageName string, imports []string, err error) { 386 fset := token.NewFileSet() 387 f, err := parser.ParseFile(fset, "<source>", source, parser.ImportsOnly) 388 if err != nil { 389 return "", nil, err 390 } 391 for _, imp := range f.Imports { 392 imports = append(imports, imp.Path.Value) 393 } 394 return f.Name.Name, imports, nil 395} 396 397func protoc(t *testing.T, args []string) { 398 cmd := exec.Command("protoc", "--plugin=protoc-gen-go="+os.Args[0]) 399 cmd.Args = append(cmd.Args, args...) 400 // We set the RUN_AS_PROTOC_GEN_GO environment variable to indicate that 401 // the subprocess should act as a proto compiler rather than a test. 402 cmd.Env = append(os.Environ(), "RUN_AS_PROTOC_GEN_GO=1") 403 out, err := cmd.CombinedOutput() 404 if len(out) > 0 || err != nil { 405 t.Log("RUNNING: ", strings.Join(cmd.Args, " ")) 406 } 407 if len(out) > 0 { 408 t.Log(string(out)) 409 } 410 if err != nil { 411 t.Fatalf("protoc: %v", err) 412 } 413} 414 415func hasReleaseTag(want string) bool { 416 for _, tag := range build.Default.ReleaseTags { 417 if tag == want { 418 return true 419 } 420 } 421 return false 422} 423