golden_test.go 9.2 KB

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