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  }