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 }