go.uber.org/yarpc@v1.72.1/encoding/thrift/thriftrw-plugin-yarpc/golden_test.go (about) 1 // Copyright (c) 2022 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package main 22 23 import ( 24 "crypto/sha1" 25 "fmt" 26 "io" 27 "io/ioutil" 28 "net" 29 "os" 30 "os/exec" 31 "path/filepath" 32 "strconv" 33 "strings" 34 "testing" 35 36 "go.uber.org/atomic" 37 "go.uber.org/thriftrw/plugin" 38 39 "github.com/stretchr/testify/assert" 40 "github.com/stretchr/testify/require" 41 ) 42 43 // This implements a test that verifies that the code in internal/tests/ is up to 44 // date. 45 46 const _testPackage = "go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests" 47 48 // Thrift files for which we set --sanitize-tchannel to true. 49 var tchannelSanitizeFor = map[string]struct{}{ 50 "weather.thrift": {}, 51 } 52 53 type fakePluginServer struct { 54 ln net.Listener 55 running atomic.Bool 56 57 // Whether the next request should use --sanitize-tchannel. 58 sanitizeTChannelNext atomic.Bool 59 } 60 61 func newFakePluginServer(t *testing.T) *fakePluginServer { 62 ln, err := net.Listen("tcp", "127.0.0.1:0") 63 require.NoError(t, err, "failed to set up TCP server") 64 65 server := &fakePluginServer{ln: ln} 66 go server.serve(t) 67 return server 68 } 69 70 func (s *fakePluginServer) Addr() string { 71 return s.ln.Addr().String() 72 } 73 74 func (s *fakePluginServer) Stop(t *testing.T) { 75 s.running.Store(false) 76 if err := s.ln.Close(); err != nil { 77 t.Logf("failed to stop fake plugin server: %v", err) 78 } 79 } 80 81 func (s *fakePluginServer) SanitizeTChannel() { 82 s.sanitizeTChannelNext.Store(true) 83 } 84 85 func (s *fakePluginServer) serve(t *testing.T) { 86 s.running.Store(true) 87 for s.running.Load() { 88 conn, err := s.ln.Accept() 89 if err != nil { 90 if s.running.Load() { 91 t.Logf("failed to open incoming connection: %v", err) 92 } 93 break 94 } 95 s.handle(conn) 96 } 97 } 98 99 func (s *fakePluginServer) handle(conn net.Conn) { 100 defer conn.Close() 101 102 // The plugin expects to close both, the reader and the writer. net.Conn 103 // doesn't like Close being called multiple times so we're going to no-op 104 // one of the closes. 105 // 106 // Additionally, the plugin server writes a response for the Goodbye 107 // request on exit. As in, 108 // 109 // plugin.Stop(): 110 // reader.Close() 111 // writer.Write(bye) 112 // writer.Close() 113 // 114 // We need the writer to be writeable after the reader.Close. So we'll 115 // no-op the reader.Close rather than writer.Close. 116 plugin.Main(&plugin.Plugin{ 117 Name: "yarpc", 118 ServiceGenerator: g{ 119 SanitizeTChannel: s.sanitizeTChannelNext.Swap(false), 120 }, 121 Reader: ioutil.NopCloser(conn), 122 Writer: conn, 123 }) 124 } 125 126 func TestCodeIsUpToDate(t *testing.T) { 127 // ThriftRW expects to call the thriftrw-plugin-yarpc binary. We trick it 128 // into calling back into this test by setting up a fake 129 // thriftrw-plugin-yarpc exectuable which uses netcat to connect back to 130 // the TCP server controlled by this test. We serve the YARPC plugin on 131 // that TCP connection. 132 // 133 // This lets us get more accurate coverage metrics for the plugin. 134 fakePlugin := newFakePluginServer(t) 135 defer fakePlugin.Stop(t) 136 { 137 tempDir, err := ioutil.TempDir("", "current-thriftrw-plugin-yarpc") 138 require.NoError(t, err, "failed to create temporary directory: %v", err) 139 defer os.RemoveAll(tempDir) 140 141 oldPath := os.Getenv("PATH") 142 newPath := fmt.Sprintf("%v:%v", tempDir, oldPath) 143 require.NoError(t, os.Setenv("PATH", newPath), 144 "failed to add %q to PATH: %v", tempDir, err) 145 defer os.Setenv("PATH", oldPath) 146 147 fakePluginPath := filepath.Join(tempDir, "thriftrw-plugin-yarpc") 148 require.NoError(t, 149 ioutil.WriteFile(fakePluginPath, callback(fakePlugin.Addr()), 0777), 150 "failed to create thriftrw plugin script") 151 } 152 153 thriftRoot, err := filepath.Abs("internal/tests") 154 require.NoError(t, err, "could not resolve absolute path to internal/tests") 155 156 thriftFiles, err := filepath.Glob(thriftRoot + "/*.thrift") 157 require.NoError(t, err) 158 159 outputDir, err := ioutil.TempDir("", "golden-test") 160 require.NoError(t, err, "failed to create temporary directory") 161 defer func() { 162 if !t.Failed() { 163 os.RemoveAll(outputDir) 164 } 165 }() 166 167 t.Logf("Created temporary output directory: %s", outputDir) 168 169 for _, thriftFile := range thriftFiles { 170 packageName := strings.TrimSuffix(filepath.Base(thriftFile), ".thrift") 171 currentPackageDir := filepath.Join("internal/tests", packageName) 172 newPackageDir := filepath.Join(outputDir, packageName) 173 174 currentHash, err := dirhash(currentPackageDir) 175 require.NoError(t, err, "could not hash %q", currentPackageDir) 176 177 _, fileName := filepath.Split(thriftFile) 178 179 // Tell the plugin whether it should --sanitize-tchannel. 180 if _, ok := tchannelSanitizeFor[fileName]; ok { 181 fakePlugin.SanitizeTChannel() 182 } 183 184 err = thriftrw( 185 "--no-recurse", 186 "--out", outputDir, 187 "--pkg-prefix", _testPackage, 188 "--thrift-root", thriftRoot, 189 "--plugin", "yarpc", 190 thriftFile, 191 ) 192 require.NoError(t, err, "failed to generate code for %q", thriftFile) 193 194 newHash, err := dirhash(newPackageDir) 195 require.NoError(t, err, "could not hash %q", newPackageDir) 196 197 assert.Equal(t, currentHash, newHash, 198 "Generated code for %q is out of date.", thriftFile) 199 } 200 } 201 202 // callback generates the contents of a script which connects back to the 203 // given TCP server. 204 func callback(addr string) []byte { 205 i := strings.LastIndexByte(addr, ':') 206 host := addr[:i] 207 port, err := strconv.ParseInt(addr[i+1:], 10, 32) 208 if err != nil { 209 panic(err) 210 } 211 212 return []byte(fmt.Sprintf(`#!/bin/bash -e 213 214 nc %v %v 215 `, host, port)) 216 } 217 218 func thriftrw(args ...string) error { 219 root, err := filepath.Abs("../../..") 220 if err != nil { 221 return fmt.Errorf("failed to resolve absolute path to project root: %v", err) 222 } 223 224 cmd := exec.Command("go", append([]string{"run", "-mod=vendor", root + "/vendor/go.uber.org/thriftrw"}, args...)...) 225 cmd.Stdout = os.Stdout 226 cmd.Stderr = os.Stderr 227 return cmd.Run() 228 } 229 230 func dirhash(dir string) (map[string]string, error) { 231 fileHashes := make(map[string]string) 232 err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 233 if err != nil { 234 return err 235 } 236 237 if info.IsDir() { 238 return nil 239 } 240 241 fileHash, err := hash(path) 242 if err != nil { 243 return fmt.Errorf("failed to hash %q: %v", path, err) 244 } 245 246 // We only care about the path relative to the directory being 247 // hashed. 248 path, err = filepath.Rel(dir, path) 249 if err != nil { 250 return err 251 } 252 if !strings.HasSuffix(path, ".nocover") { 253 fileHashes[path] = fileHash 254 } 255 return nil 256 }) 257 258 return fileHashes, err 259 } 260 261 func hash(name string) (string, error) { 262 f, err := os.Open(name) 263 if err != nil { 264 return "", err 265 } 266 defer f.Close() 267 268 h := sha1.New() 269 if _, err := io.Copy(h, f); err != nil { 270 return "", err 271 } 272 273 return fmt.Sprintf("%x", h.Sum(nil)), nil 274 }