github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/pkg/inputprocessor/inputprocessor_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 inputprocessor
    16  
    17  import (
    18  	"bufio"
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  	"testing"
    26  	"time"
    27  
    28  	lpb "github.com/bazelbuild/reclient/api/log"
    29  	ppb "github.com/bazelbuild/reclient/api/proxy"
    30  	spb "github.com/bazelbuild/reclient/api/scandeps"
    31  	"github.com/bazelbuild/reclient/internal/pkg/execroot"
    32  	"github.com/bazelbuild/reclient/internal/pkg/labels"
    33  	"github.com/bazelbuild/reclient/internal/pkg/localresources"
    34  	"github.com/bazelbuild/reclient/internal/pkg/logger"
    35  
    36  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/command"
    37  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata"
    38  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/outerr"
    39  	"github.com/bazelbuild/rules_go/go/tools/bazel"
    40  	"github.com/google/go-cmp/cmp"
    41  	"github.com/google/go-cmp/cmp/cmpopts"
    42  	"google.golang.org/grpc/codes"
    43  	"google.golang.org/grpc/status"
    44  )
    45  
    46  const (
    47  	fakeExecutionID = "fake-execution-1"
    48  	wd              = "wd"
    49  
    50  	androidCommandFilePath = "testdata/sample_android_command.txt"
    51  )
    52  
    53  var (
    54  	strSliceCmp = cmpopts.SortSlices(func(a, b string) bool { return a < b })
    55  	dsTimeout   = 10 * time.Second
    56  )
    57  
    58  func TestCpp(t *testing.T) {
    59  	ctx := context.Background()
    60  	ds := &stubCPPDependencyScanner{
    61  		processInputsReturnValue: []string{
    62  			"wd/ISoundTriggerClient.cpp",
    63  			"wd/frameworks/av/soundtrigger/ISoundTriggerClient.cpp",
    64  		},
    65  	}
    66  	resMgr := localresources.NewDefaultManager()
    67  	ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
    68  	existingFiles := []string{
    69  		filepath.Clean("wd/libc++.so.1"),
    70  		filepath.Clean("wd/ISoundTriggerClient.cpp"),
    71  		filepath.Clean("wd/frameworks/av/soundtrigger/ISoundTriggerClient.cpp"),
    72  	}
    73  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
    74  	defer cleanup()
    75  
    76  	cmd := []string{"clang++", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
    77  	lbls := map[string]string{
    78  		"type":     "compile",
    79  		"compiler": "clang",
    80  		"lang":     "cpp",
    81  		"shallow":  "false",
    82  	}
    83  	i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/libc++.so.1")}}
    84  	opts := &ProcessInputsOptions{
    85  		ExecutionID: fakeExecutionID,
    86  		Cmd:         cmd,
    87  		WorkingDir:  wd,
    88  		ExecRoot:    er,
    89  		Inputs:      i,
    90  		Labels:      lbls,
    91  	}
    92  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
    93  	if err != nil {
    94  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
    95  	}
    96  	wantIO := &CommandIO{
    97  		InputSpec:             &command.InputSpec{Inputs: existingFiles},
    98  		OutputFiles:           []string{filepath.Clean("wd/test.d"), filepath.Clean("wd/test.o")},
    99  		EmittedDependencyFile: filepath.Clean("wd/test.d"),
   100  	}
   101  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   102  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   103  	}
   104  }
   105  
   106  // TestCppEventTimes tests that the EventTime "ProcessInputs" is properly set when creating an input processor.
   107  // This was based on TestCpp and modified to test for EventTimes.
   108  func TestCppEventTimes(t *testing.T) {
   109  	ctx := context.Background()
   110  	ds := &stubCPPDependencyScanner{
   111  		processInputsReturnValue: []string{
   112  			"wd/ISoundTriggerClient.cpp",
   113  			"wd/frameworks/av/soundtrigger/ISoundTriggerClient.cpp",
   114  		},
   115  	}
   116  	resMgr := localresources.NewDefaultManager()
   117  	ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
   118  	existingFiles := []string{
   119  		filepath.Clean("wd/libc++.so.1"),
   120  		filepath.Clean("wd/ISoundTriggerClient.cpp"),
   121  		filepath.Clean("wd/frameworks/av/soundtrigger/ISoundTriggerClient.cpp"),
   122  	}
   123  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   124  	defer cleanup()
   125  
   126  	cmd := []string{"clang++", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
   127  	lbls := map[string]string{
   128  		"type":     "compile",
   129  		"compiler": "clang",
   130  		"lang":     "cpp",
   131  		"shallow":  "false",
   132  	}
   133  	i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/libc++.so.1")}}
   134  	opts := &ProcessInputsOptions{
   135  		ExecutionID: fakeExecutionID,
   136  		Cmd:         cmd,
   137  		WorkingDir:  wd,
   138  		ExecRoot:    er,
   139  		Inputs:      i,
   140  		Labels:      lbls,
   141  	}
   142  	rec := &logger.LogRecord{LogRecord: &lpb.LogRecord{}}
   143  	ip.ProcessInputs(ctx, opts, rec)
   144  	if _, ok := rec.GetLocalMetadata().GetEventTimes()["ProcessInputs"]; !ok {
   145  		t.Errorf("Event Time for %s was not set correctly", "ProcessInputs")
   146  	}
   147  }
   148  
   149  // TestCppWithDepsCache tests creating an input processor with a deps cache. It does not test the
   150  // correctness of the deps cache as this is already covered by unit tests of the depscache package.
   151  func TestCppWithDepsCache(t *testing.T) {
   152  	ctx := context.Background()
   153  	ds := &stubCPPDependencyScanner{
   154  		processInputsReturnValue: []string{
   155  			"wd/ISoundTriggerClient.cpp",
   156  			"wd/frameworks/av/soundtrigger/ISoundTriggerClient.cpp",
   157  		},
   158  	}
   159  	existingFiles := []string{
   160  		filepath.Clean("wd/libc++.so.1"),
   161  		filepath.Clean("wd/ISoundTriggerClient.cpp"),
   162  		filepath.Clean("wd/frameworks/av/soundtrigger/ISoundTriggerClient.cpp"),
   163  	}
   164  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   165  	defer cleanup()
   166  	resMgr := localresources.NewDefaultManager()
   167  	fmc := filemetadata.NewSingleFlightCache()
   168  	ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, fmc, nil)
   169  	ip.depsCache, cleanup = newDepsCache(fmc, er, nil)
   170  
   171  	cmd := []string{"clang++", "-c", "-o", "test.o",
   172  		"-Xclang", "-add-plugin", "-Xclang", "bar",
   173  		"-MF", "test.d", "test.cpp"}
   174  	lbls := map[string]string{
   175  		"type":     "compile",
   176  		"compiler": "clang",
   177  		"lang":     "cpp",
   178  		"shallow":  "false",
   179  	}
   180  	i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/libc++.so.1")}}
   181  	opts := &ProcessInputsOptions{
   182  		ExecutionID: fakeExecutionID,
   183  		Cmd:         cmd,
   184  		WorkingDir:  wd,
   185  		ExecRoot:    er,
   186  		Inputs:      i,
   187  		Labels:      lbls,
   188  	}
   189  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   190  	if err != nil {
   191  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   192  	}
   193  	wantIO := &CommandIO{
   194  		InputSpec:             &command.InputSpec{Inputs: existingFiles},
   195  		OutputFiles:           []string{filepath.Clean("wd/test.d"), filepath.Clean("wd/test.o")},
   196  		EmittedDependencyFile: filepath.Clean("wd/test.d"),
   197  	}
   198  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   199  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   200  	}
   201  	cleanup()
   202  	dcPath := filepath.Join(er, "reproxy.cache")
   203  	if _, err := os.Stat(dcPath); os.IsNotExist(err) {
   204  		t.Errorf("Deps cache file (%v) does not exist", dcPath)
   205  	}
   206  	wantDepScanArgs := []string{"-c", "-Xclang", "-add-plugin", "-Xclang", "bar", "-Qunused-arguments", "-o", "test.o", filepath.Join(er, wd, "test.cpp")}
   207  	if diff := cmp.Diff(wantDepScanArgs, ds.processInputsArgs[1:]); diff != "" {
   208  		t.Errorf("CPPDepScanner called with unexpected arguments (-want +got): %s", diff)
   209  	}
   210  }
   211  
   212  func TestCppShallowFallback(t *testing.T) {
   213  	ctx := context.Background()
   214  	ds := &stubCPPDependencyScanner{
   215  		processInputsError: errors.New("failed to call clang-scan-deps"),
   216  	}
   217  	resMgr := localresources.NewDefaultManager()
   218  	ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
   219  	existingFiles := []string{
   220  		filepath.Clean("wd/libc++.so.1"),
   221  		filepath.Clean("wd/test.cpp"),
   222  	}
   223  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   224  	defer cleanup()
   225  
   226  	cmd := []string{"clang++", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
   227  	lbls := map[string]string{
   228  		"type":     "compile",
   229  		"compiler": "clang",
   230  		"lang":     "cpp",
   231  		"shallow":  "false",
   232  	}
   233  	i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/libc++.so.1")}}
   234  	opts := &ProcessInputsOptions{
   235  		ExecutionID: fakeExecutionID,
   236  		Cmd:         cmd,
   237  		WorkingDir:  wd,
   238  		ExecRoot:    er,
   239  		Inputs:      i,
   240  		Labels:      lbls,
   241  	}
   242  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   243  	if err != nil {
   244  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   245  	}
   246  	wantIO := &CommandIO{
   247  		InputSpec:             &command.InputSpec{Inputs: existingFiles},
   248  		OutputFiles:           []string{filepath.Clean("wd/test.d"), filepath.Clean("wd/test.o")},
   249  		EmittedDependencyFile: filepath.Clean("wd/test.d"),
   250  		UsedShallowMode:       true,
   251  	}
   252  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   253  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   254  	}
   255  }
   256  
   257  func TestNoCppShallowFallbackWithRemote(t *testing.T) {
   258  	ctx := context.Background()
   259  	ds := &stubCPPDependencyScanner{
   260  		processInputsError: errors.New("failed to call clang-scan-deps"),
   261  	}
   262  	resMgr := localresources.NewDefaultManager()
   263  	ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
   264  	existingFiles := []string{
   265  		filepath.Clean("wd/libc++.so.1"),
   266  		filepath.Clean("wd/test.cpp"),
   267  	}
   268  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   269  	defer cleanup()
   270  
   271  	cmd := []string{"clang++", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
   272  	lbls := map[string]string{
   273  		"type":     "compile",
   274  		"compiler": "clang",
   275  		"lang":     "cpp",
   276  	}
   277  	i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/libc++.so.1")}}
   278  	opts := &ProcessInputsOptions{
   279  		ExecutionID:  fakeExecutionID,
   280  		Cmd:          cmd,
   281  		WorkingDir:   wd,
   282  		ExecRoot:     er,
   283  		Inputs:       i,
   284  		Labels:       lbls,
   285  		ExecStrategy: ppb.ExecutionStrategy_REMOTE,
   286  	}
   287  	if _, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}}); err == nil {
   288  		t.Errorf("ProcessInputs(%+v) did shallow fall back when remote CPP compile specified", opts)
   289  	}
   290  }
   291  
   292  func TestNoCppShallowFallbackWithRemoteLocalFallback(t *testing.T) {
   293  	ctx := context.Background()
   294  	ds := &stubCPPDependencyScanner{
   295  		processInputsError: errors.New("failed to call clang-scan-deps"),
   296  	}
   297  	resMgr := localresources.NewDefaultManager()
   298  	ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
   299  	existingFiles := []string{
   300  		filepath.Clean("wd/libc++.so.1"),
   301  		filepath.Clean("wd/test.cpp"),
   302  	}
   303  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   304  	defer cleanup()
   305  
   306  	cmd := []string{"clang++", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
   307  	lbls := map[string]string{
   308  		"type":     "compile",
   309  		"compiler": "clang",
   310  		"lang":     "cpp",
   311  	}
   312  	i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/libc++.so.1")}}
   313  	opts := &ProcessInputsOptions{
   314  		ExecutionID:  fakeExecutionID,
   315  		Cmd:          cmd,
   316  		WorkingDir:   wd,
   317  		ExecRoot:     er,
   318  		Inputs:       i,
   319  		Labels:       lbls,
   320  		ExecStrategy: ppb.ExecutionStrategy_REMOTE_LOCAL_FALLBACK,
   321  	}
   322  	if _, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}}); err == nil {
   323  		t.Errorf("ProcessInputs(%+v) did shallow fall back when remote CPP compile specified", opts)
   324  	}
   325  }
   326  
   327  func TestCppShallowFallbackWithLocal(t *testing.T) {
   328  	ctx := context.Background()
   329  	ds := &stubCPPDependencyScanner{
   330  		processInputsError: errors.New("failed to call clang-scan-deps"),
   331  	}
   332  	resMgr := localresources.NewDefaultManager()
   333  	ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
   334  	existingFiles := []string{
   335  		filepath.Clean("wd/libc++.so.1"),
   336  		filepath.Clean("wd/test.cpp"),
   337  	}
   338  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   339  	defer cleanup()
   340  
   341  	cmd := []string{"clang++", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
   342  	lbls := map[string]string{
   343  		"type":     "compile",
   344  		"compiler": "clang",
   345  		"lang":     "cpp",
   346  	}
   347  	i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/libc++.so.1")}}
   348  	opts := &ProcessInputsOptions{
   349  		ExecutionID:  fakeExecutionID,
   350  		Cmd:          cmd,
   351  		WorkingDir:   wd,
   352  		ExecRoot:     er,
   353  		Inputs:       i,
   354  		Labels:       lbls,
   355  		ExecStrategy: ppb.ExecutionStrategy_LOCAL,
   356  	}
   357  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   358  	if err != nil {
   359  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   360  	}
   361  	wantIO := &CommandIO{
   362  		InputSpec:             &command.InputSpec{Inputs: existingFiles},
   363  		OutputFiles:           []string{filepath.Clean("wd/test.d"), filepath.Clean("wd/test.o")},
   364  		EmittedDependencyFile: filepath.Clean("wd/test.d"),
   365  		UsedShallowMode:       true,
   366  	}
   367  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   368  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   369  	}
   370  }
   371  
   372  func TestHeaderABIDumper(t *testing.T) {
   373  	ctx := context.Background()
   374  	ds := &stubCPPDependencyScanner{
   375  		processInputsReturnValue: []string{
   376  			"wd/ISoundTriggerClient.cpp",
   377  			"wd/frameworks/av/soundtrigger/ISoundTriggerClient.cpp",
   378  		},
   379  	}
   380  	resMgr := localresources.NewDefaultManager()
   381  	ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
   382  	existingFiles := []string{
   383  		filepath.Clean("wd/abi-header-dumper"),
   384  		filepath.Clean("wd/libc++.so.1"),
   385  		filepath.Clean("wd/ISoundTriggerClient.cpp"),
   386  		filepath.Clean("wd/frameworks/av/soundtrigger/ISoundTriggerClient.cpp"),
   387  	}
   388  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   389  	defer cleanup()
   390  
   391  	cmd := []string{"abi-header-dumper", "-o", "test.sdump", "test.cpp", "--", "-Werror"}
   392  	lbls := map[string]string{
   393  		"type": "abi-dump",
   394  		"tool": "header-abi-dumper",
   395  	}
   396  	i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/libc++.so.1")}}
   397  	opts := &ProcessInputsOptions{
   398  		ExecutionID: fakeExecutionID,
   399  		Cmd:         cmd,
   400  		WorkingDir:  wd,
   401  		ExecRoot:    er,
   402  		Inputs:      i,
   403  		Labels:      lbls,
   404  	}
   405  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   406  	if err != nil {
   407  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   408  	}
   409  	wantIO := &CommandIO{
   410  		InputSpec:   &command.InputSpec{Inputs: existingFiles},
   411  		OutputFiles: []string{filepath.Clean("wd/test.sdump")},
   412  	}
   413  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   414  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   415  	}
   416  }
   417  
   418  func TestJavac(t *testing.T) {
   419  	ctx := context.Background()
   420  	resMgr := localresources.NewDefaultManager()
   421  	ip := newInputProcessor(nil, dsTimeout, false, nil, resMgr, nil, nil)
   422  	existingFiles := []string{
   423  		filepath.Clean("wd/foo.java"),
   424  		filepath.Clean("wd/bar/bar.java"),
   425  		filepath.Clean("wd/bar/baz.java"),
   426  	}
   427  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   428  	execroot.AddFileWithContent(t, filepath.Join(er, wd, "foo.rsp"), []byte("foo.java"))
   429  	defer cleanup()
   430  
   431  	cmd := []string{"javac", "-classpath", "bar", "-s", "out", "@foo.rsp"}
   432  	lbls := map[string]string{
   433  		"type":     "compile",
   434  		"compiler": "javac",
   435  		"lang":     "java",
   436  		"shallow":  "false",
   437  	}
   438  	i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/bar/baz.java")}}
   439  	opts := &ProcessInputsOptions{
   440  		ExecutionID: fakeExecutionID,
   441  		Cmd:         cmd,
   442  		WorkingDir:  wd,
   443  		ExecRoot:    er,
   444  		Inputs:      i,
   445  		Labels:      lbls,
   446  	}
   447  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   448  	if err != nil {
   449  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   450  	}
   451  	wantIO := &CommandIO{
   452  		InputSpec:         &command.InputSpec{Inputs: []string{filepath.Clean("wd/foo.rsp"), filepath.Clean("wd/bar"), filepath.Clean("wd/bar/baz.java"), filepath.Clean("wd/foo.java")}},
   453  		OutputDirectories: []string{filepath.Clean("wd/out")},
   454  	}
   455  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   456  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   457  	}
   458  }
   459  
   460  func TestMetalava(t *testing.T) {
   461  	ctx := context.Background()
   462  	resMgr := localresources.NewDefaultManager()
   463  	ip := newInputProcessor(nil, dsTimeout, false, &execStub{stdout: "Metalava: 1.3.0"}, resMgr, nil, nil)
   464  	existingFiles := []string{
   465  		filepath.Clean("wd/foo.java"),
   466  		filepath.Clean("wd/bar/bar.java"),
   467  		filepath.Clean("metalava.jar"),
   468  	}
   469  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   470  	execroot.AddFileWithContent(t, filepath.Join(er, wd, "foo.rsp"), []byte("foo.java"))
   471  	execroot.AddDirs(t, er, []string{"wd/src"})
   472  	defer cleanup()
   473  
   474  	cmd := []string{"metalava", "-classpath", "bar", "--api", "api.txt", "@foo.rsp", "-sourcepath", "src"}
   475  	lbls := map[string]string{
   476  		"type":     "compile",
   477  		"compiler": "metalava",
   478  		"lang":     "java",
   479  		"shallow":  "false",
   480  	}
   481  	i := &command.InputSpec{
   482  		Inputs:               []string{filepath.Clean("metalava.jar")},
   483  		EnvironmentVariables: map[string]string{"FOO": filepath.Join(er, "foo")},
   484  	}
   485  	opts := &ProcessInputsOptions{
   486  		ExecutionID: fakeExecutionID,
   487  		Cmd:         cmd,
   488  		WorkingDir:  wd,
   489  		ExecRoot:    er,
   490  		Inputs:      i,
   491  		Labels:      lbls,
   492  	}
   493  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   494  	if err != nil {
   495  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   496  	}
   497  	wantIO := &CommandIO{
   498  		InputSpec: &command.InputSpec{
   499  			Inputs: []string{filepath.Clean("wd/foo.rsp"), filepath.Clean("wd/bar"), filepath.Clean("wd/foo.java"), filepath.Clean("metalava.jar")},
   500  			VirtualInputs: []*command.VirtualInput{
   501  				&command.VirtualInput{Path: filepath.Clean("wd/src"), IsEmptyDirectory: true},
   502  			},
   503  			EnvironmentVariables: map[string]string{
   504  				"FOO": filepath.Join("..", "foo"),
   505  			},
   506  		},
   507  		OutputFiles: []string{filepath.Clean("wd/api.txt")},
   508  	}
   509  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   510  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   511  	}
   512  }
   513  
   514  func TestMetalavaInputsUnderSourcePath(t *testing.T) {
   515  	ctx := context.Background()
   516  	resMgr := localresources.NewDefaultManager()
   517  	ip := newInputProcessor(nil, dsTimeout, false, &execStub{stdout: "Metalava: 1.3.0"}, resMgr, nil, nil)
   518  	existingFiles := []string{
   519  		filepath.Clean("wd/foo.java"),
   520  		filepath.Clean("wd/src/bar/bar.java"),
   521  		filepath.Clean("metalava.jar"),
   522  	}
   523  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   524  	execroot.AddFileWithContent(t, filepath.Join(er, wd, "foo.rsp"), []byte("foo.java"))
   525  	defer cleanup()
   526  
   527  	cmd := []string{"metalava", "-classpath", "src/bar", "--api", "api.txt", "@foo.rsp", "-sourcepath", "src"}
   528  	lbls := map[string]string{
   529  		"type":     "compile",
   530  		"compiler": "metalava",
   531  		"lang":     "java",
   532  		"shallow":  "false",
   533  	}
   534  
   535  	i := &command.InputSpec{Inputs: []string{filepath.Clean("metalava.jar")}}
   536  	opts := &ProcessInputsOptions{
   537  		ExecutionID: fakeExecutionID,
   538  		Cmd:         cmd,
   539  		WorkingDir:  wd,
   540  		ExecRoot:    er,
   541  		Inputs:      i,
   542  		Labels:      lbls,
   543  	}
   544  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   545  	if err != nil {
   546  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   547  	}
   548  	wantIO := &CommandIO{
   549  		InputSpec: &command.InputSpec{
   550  			Inputs:        []string{filepath.Clean("wd/foo.rsp"), filepath.Clean("wd/src/bar"), filepath.Clean("wd/foo.java"), filepath.Clean("metalava.jar")},
   551  			VirtualInputs: []*command.VirtualInput{{Path: filepath.Clean("wd/src"), IsEmptyDirectory: true}},
   552  		},
   553  		OutputFiles: []string{filepath.Clean("wd/api.txt")},
   554  	}
   555  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   556  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   557  	}
   558  }
   559  
   560  func TestIncludeDirectoriesWithNoInputs_VirtualInputsAdded(t *testing.T) {
   561  	tests := []struct {
   562  		cmd   []string
   563  		name  string
   564  		files []string
   565  		dirs  []string
   566  		want  *command.InputSpec
   567  	}{
   568  		{
   569  			name: "Single non-existent directory",
   570  			cmd:  []string{"clang++", "-c", "-o", "test.o", "-Ia", "-MF", "test.d", "test.cpp"},
   571  			dirs: []string{"wd/a"},
   572  			want: &command.InputSpec{
   573  				Inputs: []string{filepath.Clean("wd/test.cpp")},
   574  				VirtualInputs: []*command.VirtualInput{
   575  					&command.VirtualInput{Path: filepath.Clean("wd/a"), IsEmptyDirectory: true},
   576  				},
   577  			},
   578  		},
   579  		{
   580  			name: "-I<path> follows -I <path>",
   581  			cmd:  []string{"clang++", "-c", "-o", "test.o", "-Ia", "-I", "b/c", "-MF", "test.d", "test.cpp"},
   582  			dirs: []string{"wd/a", "wd/b/c"},
   583  			want: &command.InputSpec{
   584  				Inputs: []string{filepath.Clean("wd/test.cpp")},
   585  				VirtualInputs: []*command.VirtualInput{
   586  					&command.VirtualInput{Path: filepath.Clean("wd/a"), IsEmptyDirectory: true},
   587  					&command.VirtualInput{Path: filepath.Clean("wd/b/c"), IsEmptyDirectory: true},
   588  				},
   589  			},
   590  		},
   591  		{
   592  			name: "-I <path> follows -I<path>",
   593  			cmd:  []string{"clang++", "-c", "-o", "test.o", "-I", "a/b", "-Ic/d", "-MF", "test.d", "test.cpp"},
   594  			dirs: []string{"wd/a/b", "wd/c/d"},
   595  			want: &command.InputSpec{
   596  				Inputs: []string{filepath.Clean("wd/test.cpp")},
   597  				VirtualInputs: []*command.VirtualInput{
   598  					&command.VirtualInput{Path: filepath.Clean("wd/a/b"), IsEmptyDirectory: true},
   599  					&command.VirtualInput{Path: filepath.Clean("wd/c/d"), IsEmptyDirectory: true},
   600  				},
   601  			},
   602  		},
   603  		{
   604  			name: "-isystem<path> follows -I<path>",
   605  			cmd:  []string{"clang++", "-c", "-o", "test.o", "-isystema/b", "-Ic/d", "-MF", "test.d", "test.cpp"},
   606  			dirs: []string{"wd/a/b", "wd/c/d"},
   607  			want: &command.InputSpec{
   608  				Inputs: []string{filepath.Clean("wd/test.cpp")},
   609  				VirtualInputs: []*command.VirtualInput{
   610  					&command.VirtualInput{Path: filepath.Clean("wd/a/b"), IsEmptyDirectory: true},
   611  					&command.VirtualInput{Path: filepath.Clean("wd/c/d"), IsEmptyDirectory: true},
   612  				},
   613  			},
   614  		},
   615  		{
   616  			name: "-isystem <path> follows -I<path>",
   617  			cmd:  []string{"clang++", "-c", "-o", "test.o", "-isystem", "a/b", "-Ic/d", "-MF", "test.d", "test.cpp"},
   618  			dirs: []string{"wd/a/b", "wd/c/d"},
   619  			want: &command.InputSpec{
   620  				Inputs: []string{filepath.Clean("wd/test.cpp")},
   621  				VirtualInputs: []*command.VirtualInput{
   622  					&command.VirtualInput{Path: filepath.Clean("wd/a/b"), IsEmptyDirectory: true},
   623  					&command.VirtualInput{Path: filepath.Clean("wd/c/d"), IsEmptyDirectory: true},
   624  				},
   625  			},
   626  		},
   627  		{
   628  			name: "-isystem <path> follows -isystem <path>/<subpath>",
   629  			cmd:  []string{"clang++", "-c", "-o", "test.o", "-isystem", "a/b", "-isystem", "a/b/c", "-MF", "test.d", "test.cpp"},
   630  			dirs: []string{"wd/a/b/c"},
   631  			want: &command.InputSpec{
   632  				Inputs: []string{filepath.Clean("wd/test.cpp")},
   633  				VirtualInputs: []*command.VirtualInput{
   634  					&command.VirtualInput{Path: filepath.Clean("wd/a/b"), IsEmptyDirectory: true},
   635  					&command.VirtualInput{Path: filepath.Clean("wd/a/b/c"), IsEmptyDirectory: true},
   636  				},
   637  			},
   638  		},
   639  	}
   640  
   641  	for _, test := range tests {
   642  		ctx := context.Background()
   643  		ds := &stubCPPDependencyScanner{
   644  			processInputsReturnValue: []string{"wd/test.cpp"},
   645  		}
   646  		resMgr := localresources.NewDefaultManager()
   647  		ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
   648  		existingFiles := []string{filepath.Clean("wd/test.cpp")}
   649  		er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   650  		execroot.AddFiles(t, er, test.files)
   651  		execroot.AddDirs(t, er, test.dirs)
   652  		defer cleanup()
   653  
   654  		lbls := map[string]string{
   655  			"type":     "compile",
   656  			"compiler": "clang",
   657  			"lang":     "cpp",
   658  			"shallow":  "false",
   659  		}
   660  		shallowFallbackConfig[labels.FromMap(lbls)] = map[ppb.ExecutionStrategy_Value]bool{ppb.ExecutionStrategy_UNSPECIFIED: false}
   661  		defer func() {
   662  			shallowFallbackConfig[labels.FromMap(lbls)] = map[ppb.ExecutionStrategy_Value]bool{ppb.ExecutionStrategy_UNSPECIFIED: true}
   663  		}()
   664  		i := &command.InputSpec{Inputs: []string{filepath.Clean("wd/libc++.so.1")}}
   665  		opts := &ProcessInputsOptions{
   666  			ExecutionID: fakeExecutionID,
   667  			Cmd:         test.cmd,
   668  			WorkingDir:  wd,
   669  			ExecRoot:    er,
   670  			Inputs:      i,
   671  			Labels:      lbls,
   672  		}
   673  		gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   674  		if err != nil {
   675  			t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   676  		}
   677  
   678  		if diff := cmp.Diff(test.want, gotIO.InputSpec); diff != "" {
   679  			t.Errorf("ProcessInputs(%+v) returned diff in inputs, (-want +got): %s", opts, diff)
   680  		}
   681  	}
   682  }
   683  
   684  func TestFromAndroidCompileCommand(t *testing.T) {
   685  	ctx := context.Background()
   686  	ds := &stubCPPDependencyScanner{
   687  		processInputsReturnValue: []string{
   688  			"ISoundTriggerClient.cpp",
   689  			"frameworks/av/soundtrigger/ISoundTriggerClient.cpp",
   690  			"external/libcxx/include/stdint.h",
   691  		},
   692  	}
   693  	resMgr := localresources.NewDefaultManager()
   694  	ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
   695  	existingFiles := []string{
   696  		filepath.Clean("ISoundTriggerClient.cpp"),
   697  		filepath.Clean("frameworks/av/soundtrigger/ISoundTriggerClient.cpp"),
   698  		filepath.Clean("external/libcxx/include/stdint.h"),
   699  	}
   700  	er, cleanup := execroot.Setup(t, append(existingFiles, "remote_toolchain_inputs"))
   701  	defer cleanup()
   702  
   703  	execroot.AddDirs(t, er, []string{
   704  		"system/core/libcutils/include",
   705  		"system/core/libutils/include",
   706  		"system/core/libbacktrace/include",
   707  		"system/core/liblog/include",
   708  		"system/core/libsystem/include",
   709  		"out/soong/.intermediates/frameworks/native/libs/binder/libbinder/android_arm_armv7-a-neon_core_static/gen/aidl",
   710  		"frameworks/native/libs/binder/include",
   711  		"system/core/base/include",
   712  		"external/libcxxabi/include",
   713  		"bionic/libc/system_properties/include",
   714  		"system/core/property_service/libpropertyinfoparser/include",
   715  		"system/core/include",
   716  		"system/media/audio/include",
   717  		"hardware/libhardware/include",
   718  		"hardware/libhardware_legacy/include",
   719  		"hardware/ril/include",
   720  		"libnativehelper/include",
   721  		"frameworks/native/include",
   722  		"frameworks/native/opengl/include",
   723  		"frameworks/av/include",
   724  		"bionic/libc/include",
   725  		"bionic/libc/kernel/uapi/asm-arm",
   726  		"bionic/libc/kernel/android/scsi",
   727  		"bionic/libc/kernel/android/uapi",
   728  		"libnativehelper/include_jni",
   729  	})
   730  
   731  	cmd := fromFile(t, androidCommandFilePath)
   732  	lbls := map[string]string{
   733  		"type":     "compile",
   734  		"compiler": "clang",
   735  		"lang":     "cpp",
   736  		"shallow":  "false",
   737  	}
   738  	opts := &ProcessInputsOptions{
   739  		ExecutionID: fakeExecutionID,
   740  		Cmd:         cmd,
   741  		ExecRoot:    er,
   742  		Labels:      lbls,
   743  	}
   744  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   745  	if err != nil {
   746  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   747  	}
   748  
   749  	wantInp := &command.InputSpec{
   750  		Inputs: existingFiles,
   751  		VirtualInputs: []*command.VirtualInput{
   752  			&command.VirtualInput{Path: filepath.Clean("frameworks/av/soundtrigger"), IsEmptyDirectory: true},
   753  			&command.VirtualInput{Path: filepath.Clean("system/core/libcutils/include"), IsEmptyDirectory: true},
   754  			&command.VirtualInput{Path: filepath.Clean("system/core/libutils/include"), IsEmptyDirectory: true},
   755  			&command.VirtualInput{Path: filepath.Clean("system/core/libbacktrace/include"), IsEmptyDirectory: true},
   756  			&command.VirtualInput{Path: filepath.Clean("system/core/liblog/include"), IsEmptyDirectory: true},
   757  			&command.VirtualInput{Path: filepath.Clean("system/core/libsystem/include"), IsEmptyDirectory: true},
   758  			&command.VirtualInput{Path: filepath.Clean("out/soong/.intermediates/frameworks/native/libs/binder/libbinder/android_arm_armv7-a-neon_core_static/gen/aidl"), IsEmptyDirectory: true},
   759  			&command.VirtualInput{Path: filepath.Clean("frameworks/native/libs/binder/include"), IsEmptyDirectory: true},
   760  			&command.VirtualInput{Path: filepath.Clean("system/core/base/include"), IsEmptyDirectory: true},
   761  			&command.VirtualInput{Path: filepath.Clean("external/libcxx/include"), IsEmptyDirectory: true},
   762  			&command.VirtualInput{Path: filepath.Clean("external/libcxxabi/include"), IsEmptyDirectory: true},
   763  			&command.VirtualInput{Path: filepath.Clean("bionic/libc/system_properties/include"), IsEmptyDirectory: true},
   764  			&command.VirtualInput{Path: filepath.Clean("system/core/property_service/libpropertyinfoparser/include"), IsEmptyDirectory: true},
   765  			&command.VirtualInput{Path: filepath.Clean("system/core/include"), IsEmptyDirectory: true},
   766  			&command.VirtualInput{Path: filepath.Clean("system/media/audio/include"), IsEmptyDirectory: true},
   767  			&command.VirtualInput{Path: filepath.Clean("hardware/libhardware/include"), IsEmptyDirectory: true},
   768  			&command.VirtualInput{Path: filepath.Clean("hardware/libhardware_legacy/include"), IsEmptyDirectory: true},
   769  			&command.VirtualInput{Path: filepath.Clean("hardware/ril/include"), IsEmptyDirectory: true},
   770  			&command.VirtualInput{Path: filepath.Clean("libnativehelper/include"), IsEmptyDirectory: true},
   771  			&command.VirtualInput{Path: filepath.Clean("frameworks/native/include"), IsEmptyDirectory: true},
   772  			&command.VirtualInput{Path: filepath.Clean("frameworks/native/opengl/include"), IsEmptyDirectory: true},
   773  			&command.VirtualInput{Path: filepath.Clean("frameworks/av/include"), IsEmptyDirectory: true},
   774  			&command.VirtualInput{Path: filepath.Clean("bionic/libc/include"), IsEmptyDirectory: true},
   775  			&command.VirtualInput{Path: filepath.Clean("bionic/libc/kernel/uapi"), IsEmptyDirectory: true},
   776  			&command.VirtualInput{Path: filepath.Clean("bionic/libc/kernel/uapi/asm-arm"), IsEmptyDirectory: true},
   777  			&command.VirtualInput{Path: filepath.Clean("bionic/libc/kernel/android/scsi"), IsEmptyDirectory: true},
   778  			&command.VirtualInput{Path: filepath.Clean("bionic/libc/kernel/android/uapi"), IsEmptyDirectory: true},
   779  			&command.VirtualInput{Path: filepath.Clean("libnativehelper/include_jni"), IsEmptyDirectory: true},
   780  		},
   781  	}
   782  	wantOut := []string{
   783  		filepath.Clean("out/soong/.intermediates/frameworks/av/soundtrigger/libsoundtrigger/android_arm_armv7-a-neon_core_shared/obj/frameworks/av/soundtrigger/ISoundTriggerClient.o.d"),
   784  		filepath.Clean("out/soong/.intermediates/frameworks/av/soundtrigger/libsoundtrigger/android_arm_armv7-a-neon_core_shared/obj/frameworks/av/soundtrigger/ISoundTriggerClient.o"),
   785  	}
   786  	wantIO := &CommandIO{
   787  		InputSpec:             wantInp,
   788  		OutputFiles:           wantOut,
   789  		EmittedDependencyFile: filepath.Clean("out/soong/.intermediates/frameworks/av/soundtrigger/libsoundtrigger/android_arm_armv7-a-neon_core_shared/obj/frameworks/av/soundtrigger/ISoundTriggerClient.o.d"),
   790  	}
   791  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   792  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   793  	}
   794  }
   795  
   796  func TestError(t *testing.T) {
   797  	cwd, err := os.Getwd()
   798  	if err != nil {
   799  		t.Fatalf("Unable to get working directory: %v", err)
   800  	}
   801  
   802  	tests := []struct {
   803  		name            string
   804  		executionID     string
   805  		workingDir      string
   806  		execRoot        string
   807  		inputSpec       *command.InputSpec
   808  		lbls            map[string]string
   809  		command         []string
   810  		shallowFallback bool
   811  		depsScanErr     error
   812  		wantErr         error
   813  	}{
   814  		{
   815  			name:        "ProcessInputs fails when subprocess returns error",
   816  			executionID: "test1",
   817  			workingDir:  ".",
   818  			execRoot:    cwd,
   819  			inputSpec:   &command.InputSpec{},
   820  			lbls: map[string]string{
   821  				"type":     "compile",
   822  				"compiler": "clang",
   823  				"lang":     "cpp",
   824  				"shallow":  "false",
   825  			},
   826  			command: []string{"clang++", "-c", "test.cpp"},
   827  			// expect deps scanner DeadlineExceeded to be wrapped as ErrIPTimeout
   828  			depsScanErr: context.DeadlineExceeded,
   829  			wantErr:     ErrIPTimeout,
   830  		},
   831  		{
   832  			name:        "ProcessInputs succeeds when subprocess returns error but fallback is on",
   833  			executionID: "test1",
   834  			workingDir:  ".",
   835  			execRoot:    cwd,
   836  			inputSpec:   &command.InputSpec{},
   837  			lbls: map[string]string{
   838  				"type":     "compile",
   839  				"compiler": "clang",
   840  				"lang":     "cpp",
   841  			},
   842  			command:         []string{"clang++", "-c", "test.cpp"},
   843  			shallowFallback: true,
   844  		},
   845  	}
   846  
   847  	for _, test := range tests {
   848  		t.Run(test.name, func(t *testing.T) {
   849  			ctx := context.Background()
   850  			_, cleanup := execroot.Setup(t, []string{filepath.Join(test.workingDir, "remote_toolchain_inputs")})
   851  			defer cleanup()
   852  			ds := &stubCPPDependencyScanner{processInputsError: fmt.Errorf("fail: %w", test.depsScanErr)}
   853  			resMgr := localresources.NewDefaultManager()
   854  			ip := newInputProcessor(ds, dsTimeout, false, nil, resMgr, nil, nil)
   855  			shallowFallbackConfig[labels.FromMap(test.lbls)] = map[ppb.ExecutionStrategy_Value]bool{ppb.ExecutionStrategy_UNSPECIFIED: test.shallowFallback}
   856  			defer func() {
   857  				shallowFallbackConfig[labels.FromMap(test.lbls)] = map[ppb.ExecutionStrategy_Value]bool{ppb.ExecutionStrategy_UNSPECIFIED: !test.shallowFallback}
   858  			}()
   859  
   860  			opts := &ProcessInputsOptions{
   861  				ExecutionID: test.executionID,
   862  				Cmd:         test.command,
   863  				WorkingDir:  test.workingDir,
   864  				ExecRoot:    test.execRoot,
   865  				Inputs:      test.inputSpec,
   866  				Labels:      test.lbls,
   867  			}
   868  			// verify that ProcessInputs wraps correctly dependency scanner's error
   869  			if _, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}}); !errors.Is(err, test.wantErr) {
   870  				t.Errorf("ProcessInputs(%+v)=%q, want: %q", opts, err, test.depsScanErr)
   871  			}
   872  		})
   873  	}
   874  }
   875  
   876  func TestTool(t *testing.T) {
   877  	ctx := context.Background()
   878  	ip := &InputProcessor{}
   879  	existingFiles := []string{
   880  		filepath.Join(wd, "my/bin"),
   881  		filepath.Join(wd, "input"),
   882  	}
   883  	er, cleanup := execroot.Setup(t, existingFiles)
   884  	defer cleanup()
   885  
   886  	cmd := []string{"my/bin", "input"}
   887  	lbls := map[string]string{
   888  		"type": "tool",
   889  	}
   890  	opts := &ProcessInputsOptions{
   891  		ExecutionID: fakeExecutionID,
   892  		Cmd:         cmd,
   893  		WorkingDir:  wd,
   894  		ExecRoot:    er,
   895  		Labels:      lbls,
   896  		Inputs: &command.InputSpec{
   897  			Inputs: []string{filepath.Join(wd, "input")},
   898  		},
   899  	}
   900  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   901  	if err != nil {
   902  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   903  	}
   904  	wantIO := &CommandIO{
   905  		InputSpec: &command.InputSpec{Inputs: existingFiles},
   906  	}
   907  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   908  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   909  	}
   910  }
   911  
   912  func TestSystemTool(t *testing.T) {
   913  	ctx := context.Background()
   914  	ip := &InputProcessor{}
   915  	existingFiles := []string{
   916  		filepath.Join(wd, "input"),
   917  	}
   918  	er, cleanup := execroot.Setup(t, existingFiles)
   919  	defer cleanup()
   920  
   921  	cmd := []string{"cat", "input"}
   922  	lbls := map[string]string{
   923  		"type": "tool",
   924  	}
   925  	opts := &ProcessInputsOptions{
   926  		ExecutionID: fakeExecutionID,
   927  		Cmd:         cmd,
   928  		WorkingDir:  wd,
   929  		ExecRoot:    er,
   930  		Labels:      lbls,
   931  		Inputs: &command.InputSpec{
   932  			Inputs: []string{filepath.Join(wd, "input")},
   933  		},
   934  	}
   935  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   936  	if err != nil {
   937  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   938  	}
   939  	wantIO := &CommandIO{
   940  		InputSpec: &command.InputSpec{Inputs: existingFiles},
   941  	}
   942  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   943  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   944  	}
   945  }
   946  
   947  func TestShallow(t *testing.T) {
   948  	ctx := context.Background()
   949  	resMgr := localresources.NewDefaultManager()
   950  	ip := newInputProcessor(nil, dsTimeout, false, nil, resMgr, nil, nil)
   951  	existingFiles := []string{
   952  		filepath.Clean("wd/test.cpp"),
   953  		filepath.Clean("clang++"),
   954  	}
   955  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
   956  	defer cleanup()
   957  
   958  	cmd := []string{"../clang++", "-Ifoo", "-I", "bar", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
   959  	lbls := map[string]string{
   960  		"type":     "compile",
   961  		"compiler": "clang",
   962  		"lang":     "cpp",
   963  		"shallow":  "true",
   964  	}
   965  	opts := &ProcessInputsOptions{
   966  		ExecutionID: fakeExecutionID,
   967  		Cmd:         cmd,
   968  		WorkingDir:  wd,
   969  		ExecRoot:    er,
   970  		Labels:      lbls,
   971  	}
   972  	gotIO, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
   973  	if err != nil {
   974  		t.Errorf("ProcessInputs(%+v).err = %v, want no error.", opts, err)
   975  	}
   976  	wantIO := &CommandIO{
   977  		InputSpec:             &command.InputSpec{Inputs: existingFiles},
   978  		OutputFiles:           []string{filepath.Clean("wd/test.o"), filepath.Clean("wd/test.d")},
   979  		EmittedDependencyFile: filepath.Clean("wd/test.d"),
   980  		UsedShallowMode:       true,
   981  	}
   982  	if diff := cmp.Diff(wantIO, gotIO, strSliceCmp); diff != "" {
   983  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
   984  	}
   985  }
   986  
   987  func TestError_InvalidLabels(t *testing.T) {
   988  	ctx := context.Background()
   989  	ip := &InputProcessor{}
   990  	er, cleanup := execroot.Setup(t, []string{filepath.Join(wd, "remote_toolchain_inputs")})
   991  	defer cleanup()
   992  
   993  	cmd := []string{"clang++", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
   994  	lbls := map[string]string{
   995  		"compiler": "clang",
   996  		"lang":     "cpp",
   997  	}
   998  	opts := &ProcessInputsOptions{
   999  		ExecutionID: fakeExecutionID,
  1000  		Cmd:         cmd,
  1001  		WorkingDir:  wd,
  1002  		ExecRoot:    er,
  1003  		Labels:      lbls,
  1004  	}
  1005  	if _, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}}); status.Code(err) != codes.Unimplemented {
  1006  		t.Errorf("ProcessInputs(%+v).err.Code() = %v, want %v.", opts, status.Code(err), codes.Unimplemented)
  1007  	}
  1008  }
  1009  
  1010  // Tests that the cache returns the expected result by deleting the file between first and second call
  1011  func TestFileCache(t *testing.T) {
  1012  	ctx := context.Background()
  1013  	resMgr := localresources.NewDefaultManager()
  1014  	ip := newInputProcessor(nil, dsTimeout, false, nil, resMgr, nil, nil)
  1015  	existingFiles := []string{
  1016  		filepath.Clean("wd/test.cpp"),
  1017  		filepath.Clean("clang++"),
  1018  	}
  1019  	er, cleanup := execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
  1020  	defer cleanup()
  1021  
  1022  	cmd := []string{"../clang++", "-Ifoo", "-I", "bar", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
  1023  	lbls := map[string]string{
  1024  		"type":     "compile",
  1025  		"compiler": "clang",
  1026  		"lang":     "cpp",
  1027  		"shallow":  "true",
  1028  	}
  1029  	opts := &ProcessInputsOptions{
  1030  		ExecutionID: fakeExecutionID,
  1031  		Cmd:         cmd,
  1032  		WorkingDir:  wd,
  1033  		ExecRoot:    er,
  1034  		Labels:      lbls,
  1035  	}
  1036  	// Process the inputs
  1037  	gotIOA, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
  1038  	if err != nil {
  1039  		t.Errorf("First ProcessInputs(%+v).err = %v, want no error.", opts, err)
  1040  	}
  1041  	// Cleanup the generated files
  1042  	cleanup()
  1043  	// Re-process the inputs
  1044  	gotIOB, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
  1045  	if err != nil {
  1046  		t.Errorf("Second ProcessInputs(%+v).err = %v, want no error.", opts, err)
  1047  	}
  1048  
  1049  	if diff := cmp.Diff(gotIOA, gotIOB, strSliceCmp); diff != "" {
  1050  		// If both cached, expect the results to be the same
  1051  		t.Errorf("ProcessInputs(%v) returned different results on both executions, expected identical (-first +cached): %s", opts, diff)
  1052  	}
  1053  }
  1054  
  1055  // Tests that missing files are not cached on the first call, and are returned by the second call
  1056  // after they are created.
  1057  func TestNoFileCache(t *testing.T) {
  1058  	ctx := context.Background()
  1059  	resMgr := localresources.NewDefaultManager()
  1060  	ip := newInputProcessor(nil, dsTimeout, false, nil, resMgr, nil, nil)
  1061  	existingFiles := []string{
  1062  		filepath.Clean("wd/test.cpp"),
  1063  		filepath.Clean("clang++"),
  1064  	}
  1065  	er, cleanup := execroot.Setup(t, []string{filepath.Join(wd, "remote_toolchain_inputs")})
  1066  	defer cleanup()
  1067  
  1068  	cmd := []string{"../clang++", "-Ifoo", "-I", "bar", "-c", "-o", "test.o", "-MF", "test.d", "test.cpp"}
  1069  	lbls := map[string]string{
  1070  		"type":     "compile",
  1071  		"compiler": "clang",
  1072  		"lang":     "cpp",
  1073  		"shallow":  "true",
  1074  	}
  1075  	opts := &ProcessInputsOptions{
  1076  		ExecutionID: fakeExecutionID,
  1077  		Cmd:         cmd,
  1078  		WorkingDir:  wd,
  1079  		ExecRoot:    er,
  1080  		Labels:      lbls,
  1081  	}
  1082  	// Process the inputs; expect failure because "existingFiles" do not exist
  1083  	gotIOA, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
  1084  	if err != nil {
  1085  		t.Errorf("First ProcessInputs(%+v).err = %v, want no error.", opts, err)
  1086  	}
  1087  	// Cleanup then recreate files, with the "existingFiles" this time
  1088  	cleanup()
  1089  	er, cleanup = execroot.Setup(t, append(existingFiles, filepath.Join(wd, "remote_toolchain_inputs")))
  1090  	defer cleanup() // cleanup() is a no-op if the execroot has already been cleaned up
  1091  	opts.ExecRoot = er
  1092  	// Re-process the inputs
  1093  	gotIOB, err := ip.ProcessInputs(ctx, opts, &logger.LogRecord{LogRecord: &lpb.LogRecord{}})
  1094  	cleanup()
  1095  	if err != nil {
  1096  		t.Errorf("Second ProcessInputs(%+v).err = %v, want no error.", opts, err)
  1097  	}
  1098  
  1099  	wantIO := &CommandIO{
  1100  		InputSpec:             &command.InputSpec{Inputs: existingFiles},
  1101  		OutputFiles:           []string{filepath.Clean("wd/test.o"), filepath.Clean("wd/test.d")},
  1102  		EmittedDependencyFile: filepath.Clean("wd/test.d"),
  1103  		UsedShallowMode:       true,
  1104  	}
  1105  	// A and B should be different
  1106  	if diff := cmp.Diff(gotIOA, gotIOB, strSliceCmp); diff == "" {
  1107  		// If both cached, expect the results to be the same
  1108  		t.Errorf("ProcessInputs(%v) returned identical results on both executions, expected different: %s", opts, gotIOA)
  1109  	}
  1110  	// But B should still have everything we want
  1111  	if diff := cmp.Diff(wantIO, gotIOB, strSliceCmp); diff != "" {
  1112  		t.Errorf("ProcessInputs(%v) returned diff in CommandIO, (-want +got): %s", opts, diff)
  1113  	}
  1114  }
  1115  
  1116  type stubCPPDependencyScanner struct {
  1117  	processInputsReturnValue []string
  1118  	processInputsError       error
  1119  	calls                    int
  1120  	processInputsArgs        []string
  1121  }
  1122  
  1123  func (s *stubCPPDependencyScanner) ProcessInputs(_ context.Context, _ string, args []string, _ string, _ string, _ []string) ([]string, bool, error) {
  1124  	s.processInputsArgs = args
  1125  	s.calls++
  1126  	return s.processInputsReturnValue, false, s.processInputsError
  1127  }
  1128  
  1129  func (s *stubCPPDependencyScanner) Capabilities() *spb.CapabilitiesResponse {
  1130  	return nil
  1131  }
  1132  
  1133  type execStub struct {
  1134  	stdout string
  1135  	stderr string
  1136  	err    error
  1137  }
  1138  
  1139  func (e *execStub) Execute(ctx context.Context, cmd *command.Command) (string, string, error) {
  1140  	return e.stdout, e.stderr, e.err
  1141  }
  1142  
  1143  // This is unused in inputprocessor tests but required due to depsscannerservice
  1144  func (e *execStub) ExecuteInBackground(_ context.Context, _ *command.Command, _ outerr.OutErr, _ chan *command.Result) error {
  1145  	return nil
  1146  }
  1147  
  1148  func fromFile(t *testing.T, name string) []string {
  1149  	t.Helper()
  1150  
  1151  	runfile, err := bazel.Runfile(path.Join("pkg/inputprocessor", name))
  1152  	if err == nil {
  1153  		name = runfile
  1154  	}
  1155  	f, err := os.Open(name)
  1156  	if err != nil {
  1157  		t.Fatalf("Unable to read file %v: %v", name, err)
  1158  	}
  1159  	defer f.Close()
  1160  
  1161  	var res []string
  1162  	scanner := bufio.NewScanner(f)
  1163  	for scanner.Scan() {
  1164  		res = append(res, scanner.Text())
  1165  	}
  1166  	err = scanner.Err()
  1167  	if err != nil {
  1168  		t.Fatalf("Failed to scan %s: %v", name, err)
  1169  	}
  1170  	return res
  1171  }