github.com/stackb/rules_proto@v0.0.0-20240221195024-5428336c51f1/pkg/plugintest/case.go (about)

     1  package plugintest
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/google/go-cmp/cmp"
    12  	"github.com/google/go-cmp/cmp/cmpopts"
    13  
    14  	"github.com/bazelbuild/bazel-gazelle/rule"
    15  	"github.com/stackb/rules_proto/pkg/protoc"
    16  )
    17  
    18  // Cases is a utility function that runs a mapping of test cases.
    19  func Cases(t *testing.T, subject protoc.Plugin, cases map[string]Case) {
    20  	for name, tc := range cases {
    21  		t.Run(name, func(t *testing.T) {
    22  			tc.Run(t, subject)
    23  		})
    24  	}
    25  }
    26  
    27  // Case holds the inputs and expected outputs for black-box testing of
    28  // a plugin implementation.
    29  type Case struct {
    30  	// The base name of the proto file to mock parse.  If not set, defaults to 'test' ('test.proto')
    31  	Basename string
    32  	// The relative package path
    33  	Rel string
    34  	// The configuration Name
    35  	PluginName string
    36  	// Optional directives for the package config
    37  	Directives []rule.Directive
    38  	// The input proto file source.  "syntax = proto3" will be automatically prepended.
    39  	Input string
    40  	// The expected value for the final configuration state
    41  	Configuration *protoc.PluginConfiguration
    42  	// Whether to perform integration test portion of the test
    43  	SkipIntegration bool
    44  }
    45  
    46  func (tc *Case) Run(t *testing.T, subject protoc.Plugin) {
    47  	// listFiles(".")
    48  
    49  	basename := tc.Basename
    50  	if basename == "" {
    51  		basename = "test"
    52  	}
    53  	filename := basename + ".proto"
    54  
    55  	f := protoc.NewFile(tc.Rel, filename)
    56  	in := "syntax = \"proto3\";\n\n" + tc.Input
    57  	if err := f.ParseReader(strings.NewReader(in)); err != nil {
    58  		t.Fatalf("unparseable proto file: %s: %v", tc.Input, err)
    59  	}
    60  	c := protoc.NewPackageConfig(nil)
    61  	if err := c.ParseDirectives(tc.Rel, tc.Directives); err != nil {
    62  		t.Fatalf("bad directives: %v", err)
    63  	}
    64  	if tc.PluginName == "" {
    65  		t.Fatal("test case 'PluginName' is not configured.")
    66  	}
    67  	pluginConfig, ok := c.Plugin(tc.PluginName)
    68  	if !ok {
    69  		t.Fatalf("configuration for plugin '%s' was not found", tc.PluginName)
    70  	}
    71  	r := rule.NewRule("proto_library", basename+"_proto")
    72  	lib := protoc.NewOtherProtoLibrary(nil /* File is nil, not needed for the test */, r, f)
    73  	ctx := &protoc.PluginContext{
    74  		Rel:           tc.Rel,
    75  		ProtoLibrary:  lib,
    76  		PackageConfig: *c,
    77  		PluginConfig:  pluginConfig,
    78  	}
    79  
    80  	got := subject.Configure(ctx)
    81  	want := tc.Configuration
    82  
    83  	if diff := cmp.Diff(want, got, cmpopts.EquateEmpty()); diff != "" {
    84  		t.Errorf("output configuration mismatch (-want +got): %s", diff)
    85  	}
    86  
    87  	if tc.SkipIntegration {
    88  		return
    89  	}
    90  
    91  	tc.RunIntegration(t, subject, got, filename, in)
    92  }
    93  
    94  func (tc *Case) RunIntegration(t *testing.T, subject protoc.Plugin, got *protoc.PluginConfiguration, filename, in string) {
    95  	execrootDir := os.Getenv("TEST_TMPDIR")
    96  	defer os.RemoveAll(execrootDir)
    97  	cwd, err := os.Getwd()
    98  	if err != nil {
    99  		t.Fatal(err)
   100  	}
   101  
   102  	protocPath := filepath.Join(cwd, "protoc")
   103  
   104  	// relDir is the location where the proto files are written.  A BUILD.bazel
   105  	// file containing the proto_library would normally be here.
   106  	relDir := filepath.Join(".", tc.Rel)
   107  	if err := os.MkdirAll(filepath.Join(execrootDir, relDir), os.ModePerm); err != nil {
   108  		t.Fatalf("relDir: %v", err)
   109  	}
   110  	if err := ioutil.WriteFile(filepath.Join(execrootDir, relDir, filename), []byte(in), os.ModePerm); err != nil {
   111  		t.Fatal(err)
   112  	}
   113  
   114  	// gendir is the root location where we expect generated files to be
   115  	// written.  Within a bazel action, this is the execroot unless the "Out"
   116  	// setting is configured.
   117  	outDir := "."
   118  	if got.Out != "" {
   119  		outDir = filepath.Join(outDir, got.Out)
   120  		if err := os.MkdirAll(outDir, os.ModePerm); err != nil {
   121  			t.Fatalf("outDir: %v", err)
   122  		}
   123  	}
   124  
   125  	args := []string{
   126  		"--proto_path=.", // this is the default (just a reminder)  The execroot is '.'
   127  		fmt.Sprintf("--%s_out=%s:%s", tc.PluginName, strings.Join(got.Options, ","), outDir),
   128  		filepath.Join(tc.Rel, filename),
   129  	}
   130  
   131  	t.Log("protoc args:", args)
   132  
   133  	env := []string{"PATH=.:" + cwd}
   134  	mustExecProtoc(t, protocPath, execrootDir, env, args...)
   135  
   136  	actuals := mustListFiles(t, execrootDir)
   137  	if len(tc.Configuration.Outputs) != len(actuals) {
   138  		t.Fatalf("%T.Actuals: want %d, got %d: %v", subject, len(tc.Configuration.Outputs), len(actuals), actuals)
   139  	}
   140  
   141  	for _, want := range got.Outputs {
   142  		realpath := filepath.Join(execrootDir, want)
   143  		if !fileExists(realpath) {
   144  			t.Errorf("expected file %q was not produced: (got %v)", want, actuals)
   145  		}
   146  	}
   147  }