github.com/gogo/protobuf@v1.3.2/protoc-gen-gogo/golden_test.go (about)

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