github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/internal/pkg/cppdependencyscanner/depsscannerclient/depsscannerclient_test.go (about)

     1  // Copyright 2023 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package depsscannerclient
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"os"
    21  	"path/filepath"
    22  	"runtime"
    23  	"strings"
    24  	"sync"
    25  	"sync/atomic"
    26  	"testing"
    27  	"time"
    28  
    29  	pb "github.com/bazelbuild/reclient/api/scandeps"
    30  
    31  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/command"
    32  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/outerr"
    33  	"github.com/google/go-cmp/cmp"
    34  	"github.com/google/uuid"
    35  
    36  	"google.golang.org/grpc"
    37  	"google.golang.org/protobuf/types/known/emptypb"
    38  )
    39  
    40  // A stub Executor class that records actions but does not actually execute a command.
    41  type stubExecutor struct {
    42  	// The error (if non-nil) to return from ExecuteInBackground().
    43  	err error
    44  	// Output: the Context passed to ExecuteInBackground().
    45  	ctx context.Context
    46  	// Output: the Command passed to ExecuteInBackground().
    47  	cmd *command.Command
    48  	// output: the OutErr passed to ExecuteInBackground().
    49  	oe outerr.OutErr
    50  	// output: the channel passed to ExecuteInBackground().
    51  	ch chan *command.Result
    52  }
    53  
    54  // An object for returning preset values for certain calls during New().
    55  type testService struct {
    56  	// A fake gRPC client to return from connect.
    57  	stubClient *stubClient
    58  	// The number of times connect should fail before returning stubClient.
    59  	connectDelay time.Time
    60  	// Output: the number of times connect() was called.
    61  	connectCount atomic.Int64
    62  }
    63  
    64  // connect returns a preset stubClient.
    65  func (s *testService) connect(ctx context.Context, address string) (pb.CPPDepsScannerClient, error) {
    66  	s.connectCount.Add(1)
    67  	select {
    68  	case <-time.After(time.Until(s.connectDelay)):
    69  		// Sleep, simulate a slow connection that may or may not timeout
    70  	case <-ctx.Done():
    71  		return nil, errors.New("Connection timed out")
    72  	}
    73  	if s.stubClient != nil {
    74  		return s.stubClient, nil
    75  	}
    76  	return nil, errors.New("Connection not ready yet")
    77  }
    78  
    79  // A stub CPPDepsScannerClient.
    80  type stubClient struct {
    81  	// The response to return from ProcessInputs().
    82  	processInputsResponse *pb.CPPProcessInputsResponse
    83  	// The error to return from ProcessInputs().
    84  	processInputsError error
    85  	// A delay to simulate work being done by ProcessInputs().
    86  	processInputsSleep time.Duration
    87  	// The "Status" returned from a Status call or Shutdown call.
    88  	status *pb.StatusResponse
    89  	// The error to return from a Shutdown call.
    90  	shutdownError error
    91  	// The number of times Shutdown has been called.
    92  	shutdownCalled int
    93  	// The response to return from Capabilities().
    94  	capabilitiesResponse *pb.CapabilitiesResponse
    95  	// The error to return from Capabilities().
    96  	capabilitiesError error
    97  }
    98  
    99  // Fake ProcessInputs that returns a preset value with optional delay to emulate processing time.
   100  func (c *stubClient) ProcessInputs(ctx context.Context, in *pb.CPPProcessInputsRequest, opts ...grpc.CallOption) (*pb.CPPProcessInputsResponse, error) {
   101  	select {
   102  	case <-time.After(c.processInputsSleep):
   103  		// Sleep
   104  	case <-ctx.Done():
   105  		return nil, errors.New("timeout")
   106  	}
   107  	return c.processInputsResponse, c.processInputsError
   108  }
   109  
   110  func (c *stubClient) Status(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.StatusResponse, error) {
   111  	return nil, nil
   112  }
   113  
   114  // Fake Shutdown that records a Shutdown attempt but does not actually shutdown anything.
   115  func (c *stubClient) Shutdown(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.StatusResponse, error) {
   116  	c.shutdownCalled++
   117  	return c.status, c.shutdownError
   118  }
   119  
   120  func (c *stubClient) Capabilities(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.CapabilitiesResponse, error) {
   121  	return c.capabilitiesResponse, c.capabilitiesError
   122  }
   123  
   124  // Fake Execution that performs a setup but does not actually run the command.
   125  func (e *stubExecutor) ExecuteInBackground(ctx context.Context, cmd *command.Command, oe outerr.OutErr, ch chan *command.Result) error {
   126  	e.ctx = ctx
   127  	e.cmd = cmd
   128  	e.oe = oe
   129  	e.ch = ch
   130  	return e.err
   131  }
   132  
   133  var (
   134  	directory, _ = os.Getwd()
   135  	filename     = "tests/integ/testdata/test.cpp"
   136  	// This command will not actually be used by any input processor, it is merely a prop.
   137  	compileCommand = []string{
   138  		"tests/integ/testdata/clang/bin/clang++",
   139  		"--sysroot", "tests/integ/testdata/sysroot",
   140  		"-c",
   141  		"-I",
   142  		"tests/integ/testdata/clang/include/c++/v1",
   143  		"-o", "test.obj",
   144  		filename,
   145  	}
   146  	processInputsTimeout = 100 * time.Millisecond
   147  )
   148  
   149  // TestNew_ConnectSuccess tests that a call to New() can connect to an already running dependency
   150  // scanner service.
   151  func TestNew_ConnectSuccess(t *testing.T) {
   152  	testService := &testService{
   153  		stubClient: &stubClient{},
   154  	}
   155  	oldConnect := connect
   156  	connect = testService.connect
   157  	t.Cleanup(func() {
   158  		connect = oldConnect
   159  	})
   160  	depsScannerClient, err := New(context.Background(), nil, "", 0, false, "", "127.0.0.1:8001", "127.0.0.1:1000")
   161  	if err != nil {
   162  		t.Errorf("New() retured unexpected error: %v", err)
   163  	}
   164  	if testService.connectCount.Load() != 1 {
   165  		t.Errorf("New(); expected 1 connection attempt, got %v", testService.connectCount.Load())
   166  	}
   167  	if depsScannerClient == nil {
   168  		t.Error("New(): Expected DepsScannerClient; got nil")
   169  	}
   170  }
   171  
   172  // TestNew_ConnectFailure tests that a call to New() will fail if no dependency scanner service is
   173  // running when expected.
   174  func TestNew_ConnectFailure(t *testing.T) {
   175  	setConnTimeout(t, 500*time.Millisecond)
   176  	testService := &testService{
   177  		stubClient:   nil,
   178  		connectDelay: time.Now().Add(5 * time.Second), // Wait for 5 seconds before "accepting" the connection
   179  	}
   180  	oldConnect := connect
   181  	connect = testService.connect
   182  	t.Cleanup(func() {
   183  		connect = oldConnect
   184  	})
   185  	depsScannerClient, err := New(context.Background(), nil, "", 0, false, "", "127.0.0.1:8001", "127.0.0.1:1000")
   186  	if err == nil {
   187  		t.Errorf("New() did not return expected error")
   188  	}
   189  	// Windows and mac runs are inconsistent with exactly how many attempts fit in 500ms
   190  	if testService.connectCount.Load() != 1 {
   191  		t.Errorf("New(): expected 1 connection attempt, got %v", testService.connectCount.Load())
   192  	}
   193  	if depsScannerClient != nil {
   194  		t.Error("New(): Returned DepsScannerClient; expected nil")
   195  	}
   196  }
   197  
   198  // TestNew_StartSuccess tests that a call to New() will start and connect to a dependency scanner
   199  // service executable.
   200  func TestNew_StartSuccess(t *testing.T) {
   201  	testService := &testService{
   202  		stubClient: &stubClient{},
   203  	}
   204  	oldConnect := connect
   205  	connect = testService.connect
   206  	t.Cleanup(func() {
   207  		connect = oldConnect
   208  	})
   209  	stubExecutor := &stubExecutor{}
   210  	depsScannerClient, err := New(context.Background(), stubExecutor, "", 0, false, "", "exec://test_exec", "127.0.0.1:1000")
   211  	if err != nil {
   212  		t.Errorf("New() retured unexpected error: %v", err)
   213  	}
   214  	if testService.connectCount.Load() != 1 {
   215  		t.Errorf("New(); expected 1 connection attempt, got %v", testService.connectCount.Load())
   216  	}
   217  	if depsScannerClient == nil {
   218  		t.Error("New(): Expected DepsScannerClient; got nil")
   219  	}
   220  	if stubExecutor.oe == nil {
   221  		t.Error("New(): Executor did not get an output")
   222  	}
   223  	if stubExecutor.ch == nil {
   224  		t.Error("New(): Executor did not get a channel")
   225  	}
   226  	if stubExecutor.cmd == nil {
   227  		t.Error("New(): Executor did not get a cmd")
   228  	}
   229  	if !strings.HasPrefix(depsScannerClient.address, "127.0.0.1:") {
   230  		t.Errorf("New(): Connected to %v; expected prefix %v", depsScannerClient.address, "127.0.0.1:")
   231  	}
   232  	if depsScannerClient.executable != "test_exec" {
   233  		t.Errorf("New(): Executed %v; expected %v", depsScannerClient.executable, "test_exec")
   234  	}
   235  	select {
   236  	case <-stubExecutor.ctx.Done():
   237  		t.Error("New(): Unexpected Cancel() call")
   238  	default:
   239  		// No Cancel() call. Expected.
   240  	}
   241  }
   242  
   243  // TestNew_StartFailure tests that a call to New() will fail if an executable is provided but cannot
   244  // be started.
   245  func TestNew_StartFailure(t *testing.T) {
   246  	testService := &testService{
   247  		stubClient: &stubClient{},
   248  	}
   249  	oldConnect := connect
   250  	connect = testService.connect
   251  	t.Cleanup(func() {
   252  		connect = oldConnect
   253  	})
   254  	stubExecutor := &stubExecutor{
   255  		err: errors.New("File not found"),
   256  	}
   257  	depsScannerClient, err := New(context.Background(), stubExecutor, "", 0, false, "", "exec://test_exec", "127.0.0.1:1000")
   258  	if err == nil {
   259  		t.Errorf("New() did not return expected error")
   260  	}
   261  	if testService.connectCount.Load() != 0 {
   262  		t.Errorf("New(); expected 0 connection attempts, got %v", testService.connectCount.Load())
   263  	}
   264  	if depsScannerClient != nil {
   265  		t.Error("New(): Received DepsScannerClient; expected nil")
   266  	}
   267  	if stubExecutor.oe == nil {
   268  		t.Error("New(): Executor did not get an output")
   269  	}
   270  	if stubExecutor.ch == nil {
   271  		t.Error("New(): Executor did not get a channel")
   272  	}
   273  	if stubExecutor.cmd == nil {
   274  		t.Error("New(): Executor did not get a cmd")
   275  	}
   276  	select {
   277  	case <-stubExecutor.ctx.Done():
   278  		t.Error("New(): Unexpected Cancel() call")
   279  	default:
   280  		// No Cancel() call. Expected.
   281  	}
   282  }
   283  
   284  // TestNew_StartNoConnect tests that New() will error if it is able to successfully start a
   285  // dependency scanner service, but is unable to connect to it for any reason.
   286  func TestNew_StartNoConnect(t *testing.T) {
   287  	setConnTimeout(t, 500*time.Millisecond)
   288  	testService := &testService{
   289  		connectDelay: time.Now().Add(5 * time.Second), // Wait for 5 seconds before "accepting" the connection
   290  	}
   291  	oldConnect := connect
   292  	connect = testService.connect
   293  	t.Cleanup(func() {
   294  		connect = oldConnect
   295  	})
   296  	stubExecutor := &stubExecutor{}
   297  	depsScannerClient, err := New(context.Background(), stubExecutor, "", 0, false, "", "exec://test_exec", "127.0.0.1:1000")
   298  	if err == nil {
   299  		t.Errorf("New() did not return expected error")
   300  	}
   301  	// Windows and mac runs are inconsistent with exactly how many attempts fit in 500ms
   302  	if testService.connectCount.Load() != 1 {
   303  		t.Errorf("New(): expected 1 connection attempt, got %v", testService.connectCount.Load())
   304  	}
   305  	if depsScannerClient != nil {
   306  		t.Error("New(): DepsScannerClient returned; expected nil")
   307  	}
   308  	if stubExecutor.oe == nil {
   309  		t.Error("New(): Executor did not get an output")
   310  	}
   311  	if stubExecutor.ch == nil {
   312  		t.Error("New(): Executor did not get a channel")
   313  	}
   314  	if stubExecutor.cmd == nil {
   315  		t.Error("New(): Executor did not get a cmd")
   316  	}
   317  }
   318  
   319  // TestNew_StartDelayedConnect tests that a call to New() will be successful if the started
   320  // dependency scanner service takes a little while (more than 5 seconds, less than 15) to become
   321  // available.
   322  func TestNew_StartDelayedConnect(t *testing.T) {
   323  	testService := &testService{
   324  		stubClient:   &stubClient{},
   325  		connectDelay: time.Now().Add(5 * time.Second), // Wait for 5 seconds before "accepting" the connection
   326  	}
   327  	oldConnect := connect
   328  	connect = testService.connect
   329  	t.Cleanup(func() {
   330  		connect = oldConnect
   331  	})
   332  	stubExecutor := &stubExecutor{}
   333  	depsScannerClient, err := New(context.Background(), stubExecutor, "", 0, false, "", "exec://test_exec", "127.0.0.1:1000")
   334  	if err != nil {
   335  		t.Errorf("New() retured unexpected error: %v", err)
   336  	}
   337  	if testService.connectCount.Load() != 1 {
   338  		t.Errorf("New(); expected 1 connection attempt, got %v", testService.connectCount.Load())
   339  	}
   340  	if depsScannerClient == nil {
   341  		t.Error("New(): Expected DepsScannerClient; got nil")
   342  	}
   343  	if stubExecutor.oe == nil {
   344  		t.Error("New(): Executor did not get an output")
   345  	}
   346  	if stubExecutor.ch == nil {
   347  		t.Error("New(): Executor did not get a channel")
   348  	}
   349  	if stubExecutor.cmd == nil {
   350  		t.Error("New(): Executor did not get a cmd")
   351  	}
   352  	select {
   353  	case <-stubExecutor.ctx.Done():
   354  		t.Error("New(): Unexpected Cancel() call")
   355  	default:
   356  		// No Cancel() call. Expected.
   357  	}
   358  }
   359  
   360  // TestStopService_Success tests that a call to stopService (trivially called by the exported
   361  // Close() function) will properly stop the service.
   362  func TestStopService_Success(t *testing.T) {
   363  	terminateCalled := 0
   364  	stubClient := &stubClient{}
   365  	depsScannerClient := &DepsScannerClient{
   366  		address: "127.0.0.1:8001",
   367  		ctx:     context.Background(),
   368  		terminate: func() {
   369  			terminateCalled++
   370  		},
   371  		executable: "test_exec",
   372  		oe:         outerr.NewRecordingOutErr(),
   373  		ch:         make(chan *command.Result),
   374  		client:     stubClient,
   375  	}
   376  	var wg sync.WaitGroup
   377  	var err error
   378  	wg.Add(1)
   379  	go func() {
   380  		defer wg.Done()
   381  		err = depsScannerClient.stopService(1 * time.Second)
   382  	}()
   383  
   384  	select {
   385  	case depsScannerClient.ch <- command.NewResultFromExitCode(0):
   386  		// Succesfully "stopped" the service.
   387  	case <-time.After(1 * time.Second):
   388  		t.Error("stopService was not waiting for shutdown")
   389  	}
   390  
   391  	wg.Wait()
   392  	if err != nil {
   393  		t.Error("Unexpected error shutting down service: %v", err)
   394  	}
   395  	if stubClient.shutdownCalled != 1 {
   396  		t.Error("Expected Shutdown to be called exactly once; called %v times", stubClient.shutdownCalled)
   397  	}
   398  	if terminateCalled != 0 {
   399  		t.Error("Terminate called %v times; expected 0 (clean shutdown)", terminateCalled)
   400  	}
   401  }
   402  
   403  // TestStopService_Failure tests that stopService (trivially called by the exported Close()
   404  // function) will error if it was unable to verify the service had been shutdown after a fixed
   405  // timeout.
   406  func TestStopService_Failure(t *testing.T) {
   407  	terminateCalled := 0
   408  	stubClient := &stubClient{}
   409  	depsScannerClient := &DepsScannerClient{
   410  		address: "127.0.0.1:8001",
   411  		ctx:     context.Background(),
   412  		terminate: func() {
   413  			terminateCalled++
   414  		},
   415  		executable: "test_exec",
   416  		oe:         outerr.NewRecordingOutErr(),
   417  		ch:         make(chan *command.Result),
   418  		client:     stubClient,
   419  	}
   420  	var wg sync.WaitGroup
   421  	var err error
   422  	wg.Add(1)
   423  	go func() {
   424  		defer wg.Done()
   425  		err = depsScannerClient.stopService(1 * time.Second)
   426  	}()
   427  
   428  	wg.Wait()
   429  	if err == nil {
   430  		t.Error("Error expected while shutting down; received none")
   431  	}
   432  	if stubClient.shutdownCalled != 1 {
   433  		t.Error("Expected Shutdown to be called exactly once; called %v times", stubClient.shutdownCalled)
   434  	}
   435  	if terminateCalled != 1 {
   436  		t.Error("Terminate called %v times; expected 1 (forced shutdown)", terminateCalled)
   437  	}
   438  }
   439  
   440  func TestProcessInputs_nocache(t *testing.T) {
   441  	execID := uuid.New().String()
   442  	fileList := []string{
   443  		filename,
   444  		"foo.h",
   445  		"bar.h",
   446  	}
   447  	wantDeps := []string{}
   448  	for _, d := range fileList {
   449  		wantDeps = append(wantDeps, filepath.Join(directory, d))
   450  	}
   451  	stubClient := &stubClient{
   452  		processInputsResponse: &pb.CPPProcessInputsResponse{
   453  			// Set values to verify they are not returned on error
   454  			Dependencies: fileList,
   455  			UsedCache:    false,
   456  		},
   457  	}
   458  
   459  	client := &DepsScannerClient{
   460  		ctx:    context.Background(),
   461  		client: stubClient,
   462  	}
   463  	gotDeps, cached, err := client.ProcessInputs(context.Background(), execID, compileCommand, filename, directory, []string{})
   464  	if err != nil {
   465  		t.Errorf("ProcessInputs failed: %v", err)
   466  	}
   467  	if cached != false {
   468  		t.Errorf("ProcessInputs UsedCache == true; expected false")
   469  	}
   470  	if !cmp.Equal(wantDeps, gotDeps) {
   471  		t.Errorf("ProcessInputs(%q)=%q; want %q\ndiff -want +got\n%s", filename, gotDeps, wantDeps, cmp.Diff(wantDeps, gotDeps))
   472  	}
   473  }
   474  
   475  func TestProcessInputs_abspath(t *testing.T) {
   476  	execID := uuid.New().String()
   477  	fileList := []string{
   478  		filename,
   479  		"foo.h",
   480  		"bar.h",
   481  	}
   482  	wantDeps := []string{}
   483  	for _, d := range fileList {
   484  		wantDeps = append(wantDeps, filepath.Join(directory, d))
   485  	}
   486  	absFile := "/path/to/some/file.h"
   487  	if runtime.GOOS == "windows" {
   488  		absFile = filepath.Join("C:/", absFile)
   489  	}
   490  	fileList = append(fileList, absFile)
   491  	wantDeps = append(wantDeps, absFile)
   492  
   493  	stubClient := &stubClient{
   494  		processInputsResponse: &pb.CPPProcessInputsResponse{
   495  			// Set values to verify they are not returned on error
   496  			Dependencies: fileList,
   497  		},
   498  	}
   499  
   500  	client := &DepsScannerClient{
   501  		ctx:    context.Background(),
   502  		client: stubClient,
   503  	}
   504  
   505  	gotDeps, _, err := client.ProcessInputs(context.Background(), execID, compileCommand, filename, directory, []string{})
   506  	if err != nil {
   507  		t.Errorf("ProcessInputs failed: %v", err)
   508  	}
   509  	if !cmp.Equal(wantDeps, gotDeps) {
   510  		t.Errorf("ProcessInputs(%q)=%q; want %q\ndiff -want +got\n%s", filename, gotDeps, wantDeps, cmp.Diff(wantDeps, gotDeps))
   511  	}
   512  }
   513  
   514  func TestProcessInputs_cache(t *testing.T) {
   515  	execID := uuid.New().String()
   516  	fileList := []string{
   517  		filename,
   518  		"foo.h",
   519  		"bar.h",
   520  	}
   521  	wantDeps := []string{}
   522  	for _, d := range fileList {
   523  		wantDeps = append(wantDeps, filepath.Join(directory, d))
   524  	}
   525  	stubClient := &stubClient{
   526  		processInputsResponse: &pb.CPPProcessInputsResponse{
   527  			// Set values to verify they are not returned on error
   528  			Dependencies: fileList,
   529  			UsedCache:    true,
   530  		},
   531  	}
   532  
   533  	client := &DepsScannerClient{
   534  		ctx:    context.Background(),
   535  		client: stubClient,
   536  	}
   537  
   538  	gotDeps, cached, err := client.ProcessInputs(context.Background(), execID, compileCommand, filename, directory, []string{})
   539  	if err != nil {
   540  		t.Errorf("ProcessInputs failed: %v", err)
   541  	}
   542  	if cached != true {
   543  		t.Errorf("ProcessInputs UsedCache == false; expected true")
   544  	}
   545  	if !cmp.Equal(wantDeps, gotDeps) {
   546  		t.Errorf("ProcessInputs(%q)=%q; want %q\ndiff -want +got\n%s", filename, gotDeps, wantDeps, cmp.Diff(wantDeps, gotDeps))
   547  	}
   548  }
   549  
   550  func TestProcessInputs_remoteError(t *testing.T) {
   551  	execID := uuid.New().String()
   552  	stubClient := &stubClient{
   553  		processInputsResponse: &pb.CPPProcessInputsResponse{
   554  			// Set values to verify they are not returned on error
   555  			Dependencies: []string{filename},
   556  			UsedCache:    true,
   557  		},
   558  		processInputsError: errors.New("Error"),
   559  	}
   560  
   561  	client := &DepsScannerClient{
   562  		ctx:    context.Background(),
   563  		client: stubClient,
   564  	}
   565  
   566  	gotDeps, cached, err := client.ProcessInputs(context.Background(), execID, compileCommand, filename, directory, []string{})
   567  	if err == nil {
   568  		t.Errorf("ProcessInputs succeeded; expected error")
   569  	}
   570  	if cached != false {
   571  		t.Errorf("ProcessInputs produced an error, but UsedCache is true (expected false)")
   572  	}
   573  	if gotDeps != nil {
   574  		t.Errorf("ProcessInputs produced an error, but dependencies was not nil:  %v", gotDeps)
   575  	}
   576  }
   577  
   578  func TestProcessInputs_timeoutError(t *testing.T) {
   579  	execID := uuid.New().String()
   580  	stubClient := &stubClient{
   581  		processInputsResponse: &pb.CPPProcessInputsResponse{
   582  			// Set values to verify they are not returned on error
   583  			Dependencies: []string{filename},
   584  			UsedCache:    true,
   585  		},
   586  		processInputsSleep: 2 * processInputsTimeout,
   587  	}
   588  
   589  	client := &DepsScannerClient{
   590  		ctx:    context.Background(),
   591  		client: stubClient,
   592  	}
   593  	ctx, cancel := context.WithTimeout(context.Background(), processInputsTimeout)
   594  	defer cancel()
   595  	gotDeps, cached, err := client.ProcessInputs(ctx, execID, compileCommand, filename, directory, []string{})
   596  	if err == nil {
   597  		t.Errorf("ProcessInputs succeeded; expected error")
   598  	}
   599  	if cached != false {
   600  		t.Errorf("ProcessInputs produced an error, but UsedCache is true (expected false)")
   601  	}
   602  	if gotDeps != nil {
   603  		t.Errorf("ProcessInputs produced an error, but dependencies was not nil:  %v", gotDeps)
   604  	}
   605  }
   606  
   607  func TestFindKeyVal(t *testing.T) {
   608  	tests := []struct {
   609  		name      string
   610  		envStr    string
   611  		knownVars map[string]string
   612  		wantKey   string
   613  		wantVal   string
   614  		wantErr   bool
   615  	}{{
   616  		name:   "Normal",
   617  		envStr: "Key=Val",
   618  		knownVars: map[string]string{
   619  			"Key": "Val",
   620  		},
   621  		wantKey: "Key",
   622  		wantVal: "Val",
   623  	}, {
   624  		name:   "EmptyVal",
   625  		envStr: "Key=",
   626  		knownVars: map[string]string{
   627  			"Key": "",
   628  		},
   629  		wantKey: "Key",
   630  		wantVal: "",
   631  	}, {
   632  		name:   "SpaceInVal",
   633  		envStr: "Key=Val Abc",
   634  		knownVars: map[string]string{
   635  			"Key": "Val Abc",
   636  		},
   637  		wantKey: "Key",
   638  		wantVal: "Val Abc",
   639  	}, {
   640  		name:   "EqualsInKey",
   641  		envStr: "Key=StillTheKey=Val",
   642  		knownVars: map[string]string{
   643  			"Key=StillTheKey": "Val",
   644  		},
   645  		wantKey: "Key=StillTheKey",
   646  		wantVal: "Val",
   647  	}, {
   648  		name:   "EqualsInKeyEmptyVal",
   649  		envStr: "Key=StillTheKey=",
   650  		knownVars: map[string]string{
   651  			"Key=StillTheKey": "",
   652  		},
   653  		wantKey: "Key=StillTheKey",
   654  		wantVal: "",
   655  	}, {
   656  		name:   "EqualsInVal",
   657  		envStr: "Key=Val=StillTheVal",
   658  		knownVars: map[string]string{
   659  			"Key": "Val=StillTheVal",
   660  		},
   661  		wantKey: "Key",
   662  		wantVal: "Val=StillTheVal",
   663  	}, {
   664  		name:   "EqualsInKeyOverlap",
   665  		envStr: "Key=StillTheKey=Val",
   666  		knownVars: map[string]string{
   667  			"Key":             "Val=StillTheVal",
   668  			"Key=StillTheKey": "Val",
   669  		},
   670  		wantKey: "Key=StillTheKey",
   671  		wantVal: "Val",
   672  	}, {
   673  		name:      "MissingEnvVar",
   674  		envStr:    "Key=StillTheKey=Val",
   675  		knownVars: map[string]string{},
   676  		wantKey:   "",
   677  		wantVal:   "",
   678  		wantErr:   true,
   679  	},
   680  	}
   681  	for _, test := range tests {
   682  		test := test
   683  		t.Run(test.name, func(t *testing.T) {
   684  			t.Parallel()
   685  			lookupEnvFunc := func(k string) (string, bool) {
   686  				v, ok := test.knownVars[k]
   687  				return v, ok
   688  			}
   689  			gotKey, gotVal, err := findKeyVal(test.envStr, lookupEnvFunc)
   690  			if test.wantErr && err == nil {
   691  				t.Errorf("findKeyVal(%v) did not return expected error", test.envStr)
   692  			}
   693  			if !test.wantErr && err != nil {
   694  				t.Errorf("findKeyVal(%v) returned unexpected error: %v", test.envStr, err)
   695  			}
   696  			if gotKey != test.wantKey || gotVal != test.wantVal {
   697  				t.Errorf("findKeyVal(%v) returned wrong key value pair, wanted key=%v value=%v, got key=%v value=%v", test.envStr, test.wantKey, test.wantVal, gotKey, gotVal)
   698  			}
   699  		})
   700  	}
   701  }
   702  
   703  func TestBuildAddress(t *testing.T) {
   704  	tests := []struct {
   705  		name         string
   706  		platforms    []string
   707  		serverAddr   string
   708  		openPortFunc func() (int, error)
   709  		wantAddr     string
   710  	}{
   711  		{
   712  			name:         "UnixSocketReplacereproxy",
   713  			platforms:    []string{"linux", "darwin"},
   714  			serverAddr:   "unix:///some/dir/reproxy_123.sock",
   715  			openPortFunc: func() (int, error) { return 111, nil },
   716  			wantAddr:     "unix:///some/dir/depscan_123.sock",
   717  		},
   718  		{
   719  			name:         "UnixSocketAbsOnUnix",
   720  			platforms:    []string{"linux", "darwin"},
   721  			serverAddr:   "unix:///some/dir/somesocket.sock",
   722  			openPortFunc: func() (int, error) { return 111, nil },
   723  			wantAddr:     "unix:///some/dir/ds_somesocket.sock",
   724  		},
   725  		{
   726  			name:         "UnixSocketAbsNoDirsOnUnix",
   727  			platforms:    []string{"linux", "darwin"},
   728  			serverAddr:   "unix:///somesocket.sock",
   729  			openPortFunc: func() (int, error) { return 111, nil },
   730  			wantAddr:     "unix:///ds_somesocket.sock",
   731  		},
   732  		{
   733  			name:         "UnixSocketRelNoDirsOnUnix",
   734  			platforms:    []string{"linux", "darwin"},
   735  			serverAddr:   "unix://somesocket.sock",
   736  			openPortFunc: func() (int, error) { return 111, nil },
   737  			wantAddr:     "unix://ds_somesocket.sock",
   738  		},
   739  		{
   740  			name:         "UnixSocketRelOnUnix",
   741  			platforms:    []string{"linux", "darwin"},
   742  			serverAddr:   "unix://a/b/somesocket.sock",
   743  			openPortFunc: func() (int, error) { return 111, nil },
   744  			wantAddr:     "unix://a/b/ds_somesocket.sock",
   745  		},
   746  		{
   747  			name:         "UnixSocketOnWindows",
   748  			platforms:    []string{"windows"},
   749  			serverAddr:   "unix://some/dir/somesocket.sock",
   750  			openPortFunc: func() (int, error) { return 222, nil },
   751  			wantAddr:     "127.0.0.1:222",
   752  		},
   753  		{
   754  			name:         "WindowsPipe",
   755  			platforms:    []string{"windows"},
   756  			serverAddr:   "pipe://pipename.pipe",
   757  			openPortFunc: func() (int, error) { return 333, nil },
   758  			wantAddr:     "127.0.0.1:333",
   759  		},
   760  		{
   761  			name:         "TCP",
   762  			platforms:    []string{"linux", "darwin", "windows"},
   763  			serverAddr:   "127.0.0.1:8000",
   764  			openPortFunc: func() (int, error) { return 444, nil },
   765  			wantAddr:     "127.0.0.1:444",
   766  		},
   767  		{
   768  			name:         "AnotherTCPAddress",
   769  			platforms:    []string{"linux", "darwin", "windows"},
   770  			serverAddr:   "192.168.1.1:8000",
   771  			openPortFunc: func() (int, error) { return 555, nil },
   772  			wantAddr:     "192.168.1.1:555",
   773  		},
   774  	}
   775  	for _, test := range tests {
   776  		test := test
   777  		runForPlatforms(t, test.name, test.platforms, func(t *testing.T) {
   778  			t.Parallel()
   779  			gotAddr, err := buildAddress(test.serverAddr, test.openPortFunc)
   780  			if err != nil {
   781  				t.Errorf("buildAddress(%v) returned unexpected error: %v", test.serverAddr, err)
   782  			}
   783  			if gotAddr != test.wantAddr {
   784  				t.Errorf("buildAddress(%v) returned wrong address, wanted '%v', got '%v'", test.serverAddr, test.wantAddr, gotAddr)
   785  			}
   786  		})
   787  	}
   788  }
   789  
   790  func TestCapabilities(t *testing.T) {
   791  	want := &pb.CapabilitiesResponse{
   792  		Caching:            false,
   793  		ExpectsResourceDir: true,
   794  	}
   795  	stubClient := &stubClient{
   796  		capabilitiesResponse: want,
   797  	}
   798  	client := &DepsScannerClient{
   799  		ctx:    context.Background(),
   800  		client: stubClient,
   801  	}
   802  	client.updateCapabilities(context.Background())
   803  
   804  	if got := client.Capabilities(); got != want {
   805  		t.Errorf("Capabilities() returned unexpected value, wanted %v, got %v", want, got)
   806  	}
   807  }
   808  
   809  func runForPlatforms(t *testing.T, name string, platforms []string, test func(t *testing.T)) {
   810  	t.Helper()
   811  	for _, platform := range platforms {
   812  		if runtime.GOOS == platform {
   813  			t.Run(name, test)
   814  			return
   815  		}
   816  	}
   817  }
   818  
   819  func setConnTimeout(t *testing.T, newTimeout time.Duration) {
   820  	t.Helper()
   821  	oldConnTimeout := connTimeout
   822  	connTimeout = newTimeout
   823  	t.Cleanup(func() {
   824  		connTimeout = oldConnTimeout
   825  	})
   826  }