kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/platform/analysis/proxy/proxy_test.go (about) 1 /* 2 * Copyright 2017 The Kythe Authors. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package proxy 18 19 import ( 20 "encoding/json" 21 "errors" 22 "io" 23 "testing" 24 25 "kythe.io/kythe/go/test/testutil" 26 "kythe.io/kythe/go/util/compare" 27 "kythe.io/kythe/go/util/log" 28 "kythe.io/kythe/go/util/schema/facts" 29 30 "google.golang.org/protobuf/encoding/protojson" 31 "google.golang.org/protobuf/proto" 32 33 apb "kythe.io/kythe/proto/analysis_go_proto" 34 cpb "kythe.io/kythe/proto/common_go_proto" 35 spb "kythe.io/kythe/proto/storage_go_proto" 36 ) 37 38 // A testreq is the equivalent of a request, pre-encoding. 39 type testreq struct { 40 Type string `json:"req,omitempty"` 41 Args any `json:"args,omitempty"` 42 } 43 44 // An indexer simulates an indexer process. It sends each of the requests in 45 // its queue in turn, and records the responses. 46 type indexer struct { 47 in *json.Decoder 48 out *json.Encoder 49 50 reqs []testreq 51 rsps []string // JSON format 52 errc chan error 53 } 54 55 func (ix *indexer) run() { 56 defer close(ix.errc) 57 for _, req := range ix.reqs { 58 if err := ix.out.Encode(req); err != nil { 59 ix.errc <- err 60 return 61 } 62 63 var rsp response 64 if err := ix.in.Decode(&rsp); err != nil { 65 ix.errc <- err 66 return 67 } 68 69 jrsp := mustMarshal(&rsp) 70 ix.rsps = append(ix.rsps, jrsp) 71 } 72 } 73 74 func newIndexer(in io.Reader, out io.Writer, reqs ...testreq) *indexer { 75 return &indexer{ 76 in: json.NewDecoder(in), 77 out: json.NewEncoder(out), 78 reqs: reqs, 79 errc: make(chan error, 1), 80 } 81 } 82 83 func (ix *indexer) wait() error { return <-ix.errc } 84 85 type handler struct { 86 analysis func() (*apb.AnalysisRequest, error) 87 output func(...*spb.Entry) error 88 done func(error) 89 file func(path, digest string) ([]byte, error) 90 } 91 92 // Analysis implements part of the Handler interface. If no function is set, 93 // testReq is returned without error. 94 func (h handler) Analysis() (*apb.AnalysisRequest, error) { 95 if f := h.analysis; f != nil { 96 return f() 97 } 98 return testReq, nil 99 } 100 101 // Output implements part of the Handler interface. If no function is set, the 102 // output is discarded without error. 103 func (h handler) Output(entries ...*spb.Entry) error { 104 if f := h.output; f != nil { 105 return f(entries...) 106 } 107 return nil 108 } 109 110 // Done implements part of the Handler interface. If no function is set, this 111 // is silently a no-op. 112 func (h handler) Done(err error) { 113 if f := h.done; f != nil { 114 f(err) 115 } 116 } 117 118 // File implements part of the Handler interface. If no function is set, an 119 // error with string "notfound" is returned. 120 func (h handler) File(path, digest string) ([]byte, error) { 121 if f := h.file; f != nil { 122 return f(path, digest) 123 } 124 return nil, errors.New("notfound") 125 } 126 127 // runProxy runs an indexer with the given requests and a proxy delegating to 128 // h. The responses collected by the indexer are returned. 129 func runProxy(h Handler, reqs ...testreq) ([]string, error) { 130 pin, pout := io.Pipe() // proxy to indexer 131 xin, xout := io.Pipe() // indexer to proxy 132 ix := newIndexer(xin, pout, reqs...) 133 go func() { 134 defer pout.Close() // signal EOF to the driver 135 ix.run() 136 }() 137 err := New(pin, xout).Run(h) 138 if err != nil { 139 xout.Close() 140 } 141 if xerr := ix.wait(); err == nil { 142 err = xerr 143 } 144 return ix.rsps, err 145 } 146 147 // Dummy values for testing. 148 var ( 149 testReq = &apb.AnalysisRequest{ 150 Compilation: &apb.CompilationUnit{ 151 VName: &spb.VName{Signature: "test"}, 152 }, 153 Revision: "1", 154 FileDataService: "Q", 155 } 156 157 testEntries = []*spb.Entry{ 158 {Source: &spb.VName{Signature: "A"}, EdgeKind: "loves", Target: &spb.VName{Signature: "B"}}, 159 {Source: &spb.VName{Signature: "C"}, FactName: "versus", FactValue: []byte("D")}, 160 } 161 ) 162 163 const analysisReply = `{"rsp":"ok","args":{"fds":"Q","rev":"1","unit":{"v_name":{"signature":"test"}}}}` 164 const analysisWireReply = `{"rsp":"ok","args":{"fds":"Q","rev":"1","unit":"CgYKBHRlc3Q="}}` 165 166 func mustMarshal(v any) string { 167 bits, err := json.Marshal(v) 168 if err != nil { 169 log.Fatalf("Error marshaling JSON: %v", err) 170 } 171 return string(bits) 172 } 173 174 func encodeEntries(es []*spb.Entry) json.RawMessage { 175 var messages []json.RawMessage 176 for _, e := range es { 177 rec, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(e) 178 if err != nil { 179 panic(err) 180 } 181 messages = append(messages, rec) 182 } 183 184 msg, err := json.Marshal(messages) 185 if err != nil { 186 panic(err) 187 } 188 return msg 189 } 190 191 func encodeWireEntries(es []*spb.Entry) json.RawMessage { 192 var messages [][]byte 193 for _, e := range es { 194 rec, err := proto.MarshalOptions{}.Marshal(e) 195 if err != nil { 196 panic(err) 197 } 198 messages = append(messages, rec) 199 } 200 201 msg, err := json.Marshal(messages) 202 if err != nil { 203 panic(err) 204 } 205 return msg 206 } 207 208 func TestNOOP(t *testing.T) { 209 // Verify that startup and shutdown are clean. 210 if rsps, err := runProxy(handler{}); err != nil { 211 t.Errorf("Proxy failed on empty input: %v", err) 212 } else if len(rsps) != 0 { 213 t.Errorf("Empty input returned responses: %+v", rsps) 214 } 215 } 216 217 func TestErrors(t *testing.T) { 218 tests := []struct { 219 desc string 220 h Handler 221 reqs []testreq 222 want []string 223 }{{ 224 desc: "Error getting analysis", 225 h: handler{analysis: func() (*apb.AnalysisRequest, error) { return nil, errors.New("bad") }}, 226 reqs: []testreq{{Type: "analysis"}}, 227 want: []string{`{"rsp":"error","args":"bad"}`}, 228 }, { 229 desc: "Error sending outputs", 230 h: handler{output: func(...*spb.Entry) error { return errors.New("bad") }}, 231 reqs: []testreq{{Type: "analysis"}, {Type: "output", Args: encodeEntries(testEntries)}}, 232 want: []string{ 233 analysisReply, 234 `{"rsp":"error","args":"bad"}`, 235 }, 236 }, { 237 desc: "Requests sent out of order provoke an error", 238 h: handler{}, 239 reqs: []testreq{ 240 {Type: "done", Args: status{OK: true}}, 241 {Type: "output", Args: encodeEntries(testEntries)}, 242 }, 243 want: []string{ 244 `{"rsp":"error","args":"no analysis is in progress"}`, 245 `{"rsp":"error","args":"no analysis is in progress"}`, 246 }, 247 }, { 248 desc: "File not found", 249 h: handler{}, 250 reqs: []testreq{{Type: "file", Args: file{Path: "foo", Digest: "bar"}}}, 251 want: []string{`{"rsp":"error","args":"notfound"}`}, 252 }, { 253 desc: "An error in output terminates the analysis", 254 h: handler{ 255 output: func(es ...*spb.Entry) error { 256 if es[0].EdgeKind == "fail" { 257 return errors.New("bad") 258 } 259 return nil 260 }, 261 }, 262 reqs: []testreq{ 263 {Type: "analysis"}, // succeeds 264 {Type: "output", Args: encodeEntries([]*spb.Entry{{EdgeKind: "ok"}})}, // succeeds 265 {Type: "output", Args: encodeEntries([]*spb.Entry{{EdgeKind: "fail"}})}, // fails 266 {Type: "output", Args: encodeEntries([]*spb.Entry{{EdgeKind: "wah"}})}, // fails 267 {Type: "done", Args: status{OK: false, Message: "cat abuse"}}, // fails 268 {Type: "analysis"}, // succeeds 269 }, 270 want: []string{ 271 analysisReply, 272 `{"rsp":"ok"}`, 273 `{"rsp":"error","args":"bad"}`, 274 `{"rsp":"error","args":"no analysis is in progress"}`, 275 `{"rsp":"error","args":"no analysis is in progress"}`, 276 analysisReply, 277 }, 278 }} 279 280 for _, test := range tests { 281 t.Log("Testing:", test.desc) 282 t.Logf(" - requests: %#q", test.reqs) 283 rsps, err := runProxy(test.h, test.reqs...) 284 if err != nil { 285 t.Errorf("Unexpected error from proxy: %v", err) 286 } 287 t.Logf(" - responses: %+v", rsps) 288 if diff := compare.ProtoDiff(rsps, test.want); diff != "" { 289 t.Errorf("Incorrect responses; wanted %+v: %s", test.want, diff) 290 } 291 } 292 } 293 294 func TestAnalysisWorks(t *testing.T) { 295 // Enact a standard analyzer transaction, and verify that the responses work. 296 var doneCalled bool 297 var gotEntries []*spb.Entry 298 299 rsps, err := runProxy(handler{ 300 done: func(err error) { 301 if err != nil { 302 t.Errorf("Done was called with an error: %v", err) 303 } 304 doneCalled = true 305 }, 306 output: func(es ...*spb.Entry) error { 307 gotEntries = append(gotEntries, es...) 308 return nil 309 }, 310 file: func(path, digest string) ([]byte, error) { 311 if path == "exists" { 312 return []byte("data"), nil 313 } 314 return nil, errors.New("notfound") 315 }, 316 }, 317 testreq{Type: "analysis"}, 318 testreq{Type: "file", Args: file{Path: "exists"}}, 319 testreq{Type: "output", Args: encodeEntries(testEntries)}, 320 testreq{Type: "file", Args: file{Path: "does not exist"}}, 321 testreq{Type: "done"}, 322 ) 323 if err != nil { 324 t.Errorf("Unexpected error from proxy: %v", err) 325 } 326 327 // Verify that the proxy followed the protocol reasonably. 328 if !doneCalled { 329 t.Error("The handler's Done method was never called") 330 } 331 if diff := compare.ProtoDiff(gotEntries, testEntries); diff != "" { 332 t.Errorf("Incorrect entries:\n got: %+v\nwant: %+v: %s", gotEntries, testEntries, diff) 333 } 334 335 // Verify that we got the expected replies back from the proxy. 336 want := []string{ 337 analysisReply, // from Analyze 338 `{"rsp":"ok","args":{"content":"ZGF0YQ==","path":"exists"}}`, // from File (1) 339 `{"rsp":"ok"}`, // from Output 340 `{"rsp":"error","args":"notfound"}`, // from File (2) 341 `{"rsp":"ok"}`, // from Done 342 } 343 if diff := compare.ProtoDiff(rsps, want); diff != "" { 344 t.Errorf("Wrong incorrect responses:\n got: %+v\nwant: %+v: %s", rsps, want, diff) 345 } 346 } 347 348 func TestCodeJSON(t *testing.T) { 349 ms := &cpb.MarkedSource{} 350 json, err := protojson.Marshal(ms) 351 testutil.Fatalf(t, "protojson.Marshal: %v", err) 352 rec, err := proto.Marshal(ms) 353 testutil.Fatalf(t, "proto.Marshal: %v", err) 354 355 testEntries := []*spb.Entry{ 356 &spb.Entry{ 357 Source: &spb.VName{Signature: "sig"}, 358 FactName: codeJSONFact, 359 FactValue: json, 360 }, 361 } 362 expectedEntries := []*spb.Entry{ 363 &spb.Entry{ 364 Source: &spb.VName{Signature: "sig"}, 365 FactName: facts.Code, 366 FactValue: rec, 367 }, 368 } 369 370 var gotEntries []*spb.Entry 371 372 rsps, err := runProxy(handler{ 373 done: func(err error) { 374 if err != nil { 375 t.Errorf("Done was called with an error: %v", err) 376 } 377 }, 378 output: func(es ...*spb.Entry) error { 379 gotEntries = append(gotEntries, es...) 380 return nil 381 }, 382 file: func(path, digest string) ([]byte, error) { 383 if path == "exists" { 384 return []byte("data"), nil 385 } 386 return nil, errors.New("notfound") 387 }, 388 }, 389 testreq{Type: "analysis"}, 390 testreq{Type: "file", Args: file{Path: "exists"}}, 391 testreq{Type: "output", Args: encodeEntries(testEntries)}, 392 testreq{Type: "file", Args: file{Path: "does not exist"}}, 393 testreq{Type: "done"}, 394 ) 395 if err != nil { 396 t.Errorf("Unexpected error from proxy: %v", err) 397 } 398 399 if diff := compare.ProtoDiff(gotEntries, expectedEntries); diff != "" { 400 t.Errorf("Incorrect entries:\n got: %+v\nwant: %+v: %s", gotEntries, testEntries, diff) 401 } 402 403 // Verify that we got the expected replies back from the proxy. 404 want := []string{ 405 analysisReply, // from Analyze 406 `{"rsp":"ok","args":{"content":"ZGF0YQ==","path":"exists"}}`, // from File (1) 407 `{"rsp":"ok"}`, // from Output 408 `{"rsp":"error","args":"notfound"}`, // from File (2) 409 `{"rsp":"ok"}`, // from Done 410 } 411 if diff := compare.ProtoDiff(rsps, want); diff != "" { 412 t.Errorf("Wrong incorrect responses:\n got: %+v\nwant: %+v: %s", rsps, want, diff) 413 } 414 } 415 416 func TestWireAnalysisWorks(t *testing.T) { 417 // Enact a standard analyzer transaction, and verify that the responses work. 418 var doneCalled bool 419 var gotEntries []*spb.Entry 420 421 rsps, err := runProxy(handler{ 422 done: func(err error) { 423 if err != nil { 424 t.Errorf("Done was called with an error: %v", err) 425 } 426 doneCalled = true 427 }, 428 output: func(es ...*spb.Entry) error { 429 gotEntries = append(gotEntries, es...) 430 return nil 431 }, 432 file: func(path, digest string) ([]byte, error) { 433 if path == "exists" { 434 return []byte("data"), nil 435 } 436 return nil, errors.New("notfound") 437 }, 438 }, 439 testreq{Type: "analysis_wire"}, 440 testreq{Type: "file", Args: file{Path: "exists"}}, 441 testreq{Type: "output_wire", Args: encodeWireEntries(testEntries)}, 442 testreq{Type: "file", Args: file{Path: "does not exist"}}, 443 testreq{Type: "done"}, 444 ) 445 if err != nil { 446 t.Errorf("Unexpected error from proxy: %v", err) 447 } 448 449 // Verify that the proxy followed the protocol reasonably. 450 if !doneCalled { 451 t.Error("The handler's Done method was never called") 452 } 453 if diff := compare.ProtoDiff(gotEntries, testEntries); diff != "" { 454 t.Errorf("Incorrect entries:\n got: %+v\nwant: %+v: %s", gotEntries, testEntries, diff) 455 } 456 457 // Verify that we got the expected replies back from the proxy. 458 want := []string{ 459 analysisWireReply, // from Analyze Wire 460 `{"rsp":"ok","args":{"content":"ZGF0YQ==","path":"exists"}}`, // from File (1) 461 `{"rsp":"ok"}`, // from Output Wire 462 `{"rsp":"error","args":"notfound"}`, // from File (2) 463 `{"rsp":"ok"}`, // from Done 464 } 465 if diff := compare.ProtoDiff(rsps, want); diff != "" { 466 t.Errorf("Wrong incorrect responses:\n got: %+v\nwant: %+v: %s", rsps, want, diff) 467 } 468 }