github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/internal/pkg/inputprocessor/action/cppcompile/preprocessor_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 cppcompile
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"sync"
    23  	"testing"
    24  
    25  	spb "github.com/bazelbuild/reclient/api/scandeps"
    26  	"github.com/bazelbuild/reclient/internal/pkg/execroot"
    27  	"github.com/bazelbuild/reclient/internal/pkg/inputprocessor"
    28  	"github.com/bazelbuild/reclient/internal/pkg/inputprocessor/depscache"
    29  	"github.com/bazelbuild/reclient/internal/pkg/inputprocessor/flags"
    30  
    31  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/command"
    32  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata"
    33  	"github.com/google/go-cmp/cmp"
    34  	"github.com/google/go-cmp/cmp/cmpopts"
    35  )
    36  
    37  var (
    38  	strSliceCmp = cmpopts.SortSlices(func(a, b string) bool { return a < b })
    39  )
    40  
    41  func TestComputeSpec(t *testing.T) {
    42  	tests := []struct {
    43  		name                 string
    44  		scandepsCapabilities *spb.CapabilitiesResponse
    45  		printResDirOut       string
    46  		inputExtraCmd        []string
    47  		wantResDir           string
    48  	}{
    49  		{
    50  			name:                 "ResourceDirProvided/ExpectsResourceDir",
    51  			scandepsCapabilities: &spb.CapabilitiesResponse{ExpectsResourceDir: true},
    52  			printResDirOut:       "  /some/other/dir   ",
    53  			inputExtraCmd:        []string{"-resource-dir", "/some/dir"},
    54  			wantResDir:           "/some/dir",
    55  		},
    56  		{
    57  			name:                 "ResourceDirProvided/DoesntExpectResourceDir",
    58  			scandepsCapabilities: &spb.CapabilitiesResponse{ExpectsResourceDir: false},
    59  			printResDirOut:       "  /some/other/dir   ",
    60  			inputExtraCmd:        []string{"-resource-dir", "/some/dir"},
    61  			wantResDir:           "/some/dir",
    62  		},
    63  		{
    64  			name:                 "ResourceDirMissing/ExpectsResourceDir",
    65  			scandepsCapabilities: &spb.CapabilitiesResponse{ExpectsResourceDir: true},
    66  			printResDirOut:       "  /some/other/dir   ",
    67  			wantResDir:           "/some/other/dir",
    68  		},
    69  		{
    70  			name:                 "ResourceDirMissing/DoesntExpectResourceDir",
    71  			scandepsCapabilities: &spb.CapabilitiesResponse{ExpectsResourceDir: false},
    72  			printResDirOut:       "  /some/other/dir   ",
    73  		},
    74  	}
    75  
    76  	for _, tc := range tests {
    77  		tc := tc
    78  		t.Run(tc.name, func(t *testing.T) {
    79  			ctx := context.Background()
    80  			fmc := filemetadata.NewSingleFlightCache()
    81  			s := &stubCPPDepScanner{
    82  				res:          []string{"include/foo.h"},
    83  				err:          nil,
    84  				capabilities: tc.scandepsCapabilities,
    85  			}
    86  			c := &Preprocessor{
    87  				CPPDepScanner: s,
    88  				BasePreprocessor: &inputprocessor.BasePreprocessor{
    89  					Ctx:               ctx,
    90  					FileMetadataCache: fmc,
    91  					Executor:          &stubExecutor{outStr: tc.printResDirOut},
    92  				},
    93  			}
    94  
    95  			// Tests that virtual inputs only include existing directories and excludes files.
    96  			existingFiles := []string{
    97  				filepath.Clean("bin/clang++"),
    98  				filepath.Clean("src/test.cpp"),
    99  				filepath.Clean("include/foo/a"),
   100  				filepath.Clean("include/a/b.hmap"),
   101  				filepath.Clean("out/dummy"),
   102  			}
   103  			er, cleanup := execroot.Setup(t, existingFiles)
   104  			defer cleanup()
   105  			inputs := []string{filepath.Clean("include/a/b.hmap")}
   106  			opts := inputprocessor.Options{
   107  				Cmd: append([]string{"../bin/clang++", "-o", "test.o", "-MF", "test.d", "-I../include/foo", "-I../include/bar", "-I../include/a/b.hmap",
   108  					"-std=c++14", "-Xclang", "-verify", "-c", "../src/test.cpp"}, tc.inputExtraCmd...),
   109  				WorkingDir: "out",
   110  				ExecRoot:   er,
   111  				Labels:     map[string]string{"type": "compile", "compiler": "clang", "lang": "cpp"},
   112  				Inputs: &command.InputSpec{
   113  					Inputs: inputs,
   114  				},
   115  			}
   116  			got, err := inputprocessor.Compute(c, opts)
   117  			if err != nil {
   118  				t.Errorf("Spec() failed: %v", err)
   119  			}
   120  			want := &command.InputSpec{
   121  				Inputs: []string{
   122  					filepath.Clean("src/test.cpp"),
   123  					filepath.Clean("bin/clang++"),
   124  					filepath.Clean("include/a/b.hmap"),
   125  				},
   126  				VirtualInputs: []*command.VirtualInput{
   127  					{Path: filepath.Clean("include/foo"), IsEmptyDirectory: true},
   128  				},
   129  			}
   130  			if diff := cmp.Diff(want, got.InputSpec, strSliceCmp); diff != "" {
   131  				t.Errorf("Compute() returned diff (-want +got): %v", diff)
   132  			}
   133  			wantCmd := []string{
   134  				filepath.Join(er, "bin/clang++"),
   135  				"-I../include/foo",
   136  				"-I../include/bar",
   137  				"-I../include/a/b.hmap",
   138  				"-std=c++14", "-c", // expect -std=xx to not be normalized
   139  				"-Qunused-arguments", // expect Qunused-arguments to be added, and -Xclang -verify to be removed
   140  				"-o", "test.o",
   141  				filepath.Join(er, "src/test.cpp"),
   142  			}
   143  			gotCmdNoResDir := make([]string, 0, len(s.gotCmd))
   144  			i := 0
   145  			gotResDir := ""
   146  			for i < len(s.gotCmd) {
   147  				if s.gotCmd[i] == "-resource-dir" {
   148  					i++
   149  					gotResDir = s.gotCmd[i]
   150  				} else {
   151  					gotCmdNoResDir = append(gotCmdNoResDir, s.gotCmd[i])
   152  				}
   153  				i++
   154  			}
   155  			if diff := cmp.Diff(wantCmd, gotCmdNoResDir); diff != "" {
   156  				t.Errorf("Unexpected command passed to the dependency scanner (-want +got): %v", diff)
   157  			}
   158  			if tc.wantResDir != gotResDir {
   159  				t.Errorf("CPP command had incorrect resource dir, wanted %v, got %v", tc.wantResDir, gotResDir)
   160  			}
   161  		})
   162  	}
   163  }
   164  
   165  func TestComputeSpecWithDepsCache(t *testing.T) {
   166  	ctx := context.Background()
   167  	fmc := filemetadata.NewSingleFlightCache()
   168  	s := &stubCPPDepScanner{
   169  		res: []string{"include/foo.h"},
   170  		err: nil,
   171  	}
   172  	c := &Preprocessor{
   173  		CPPDepScanner:    s,
   174  		BasePreprocessor: &inputprocessor.BasePreprocessor{Ctx: ctx, FileMetadataCache: fmc},
   175  		DepsCache:        depscache.New(filemetadata.NewSingleFlightCache()),
   176  	}
   177  
   178  	existingFiles := []string{
   179  		filepath.Clean("bin/clang++"),
   180  		filepath.Clean("src/test.cpp"),
   181  		filepath.Clean("include/foo/a"),
   182  		filepath.Clean("out/dummy"),
   183  	}
   184  	er, cleanup := execroot.Setup(t, existingFiles)
   185  	defer cleanup()
   186  	c.DepsCache.LoadFromDir(er)
   187  	opts := inputprocessor.Options{
   188  		Cmd:        []string{"../bin/clang++", "-o", "test.o", "-MF", "test.d", "-I../include/foo", "-I", "../include/bar", "-c", "../src/test.cpp"},
   189  		WorkingDir: "out",
   190  		ExecRoot:   er,
   191  		Labels:     map[string]string{"type": "compile", "compiler": "clang", "lang": "cpp"},
   192  	}
   193  	var wg sync.WaitGroup
   194  	wg.Add(1)
   195  	c.testOnlySetDone = func() { wg.Done() }
   196  	got, err := inputprocessor.Compute(c, opts)
   197  	if err != nil {
   198  		t.Errorf("Compute() failed: %v", err)
   199  	}
   200  	want := &command.InputSpec{
   201  		Inputs: []string{
   202  			filepath.Clean("src/test.cpp"),
   203  			filepath.Clean("bin/clang++"),
   204  		},
   205  		VirtualInputs: []*command.VirtualInput{
   206  			{Path: filepath.Clean("include/foo"), IsEmptyDirectory: true},
   207  		},
   208  	}
   209  	if diff := cmp.Diff(want, got.InputSpec, strSliceCmp); diff != "" {
   210  		t.Errorf("Compute() returned diff (-want +got): %v", diff)
   211  	}
   212  	wg.Wait()
   213  	c.DepsCache.WriteToDisk(er)
   214  	c = &Preprocessor{
   215  		CPPDepScanner:    s,
   216  		BasePreprocessor: &inputprocessor.BasePreprocessor{Ctx: ctx, FileMetadataCache: fmc},
   217  		DepsCache:        depscache.New(filemetadata.NewSingleFlightCache()),
   218  	}
   219  	c.DepsCache.LoadFromDir(er)
   220  	got, err = inputprocessor.Compute(c, opts)
   221  	if err != nil {
   222  		t.Errorf("Compute() failed: %v", err)
   223  	}
   224  	if diff := cmp.Diff(want, got.InputSpec, strSliceCmp); diff != "" {
   225  		t.Errorf("Compute() returned diff (-want +got): %v", diff)
   226  	}
   227  	if s.processCalls != 1 {
   228  		t.Errorf("Wrong number of ProcessInputs calls: got %v, want 1", s.processCalls)
   229  	}
   230  }
   231  
   232  func TestComputeSpecWithDepsCache_ResourceDirChanged(t *testing.T) {
   233  	ctx := context.Background()
   234  	fmc := filemetadata.NewSingleFlightCache()
   235  	s := &stubCPPDepScanner{
   236  		res:          []string{"include/foo.h"},
   237  		err:          nil,
   238  		capabilities: &spb.CapabilitiesResponse{ExpectsResourceDir: true},
   239  	}
   240  	c := &Preprocessor{
   241  		CPPDepScanner: s,
   242  		BasePreprocessor: &inputprocessor.BasePreprocessor{
   243  			Ctx:               ctx,
   244  			FileMetadataCache: fmc,
   245  			Executor:          &stubExecutor{outStr: "/first/resource/dir"},
   246  		},
   247  		DepsCache: depscache.New(filemetadata.NewSingleFlightCache()),
   248  	}
   249  
   250  	existingFiles := []string{
   251  		filepath.Clean("bin/clang++"),
   252  		filepath.Clean("src/test.cpp"),
   253  		filepath.Clean("include/foo/a"),
   254  		filepath.Clean("out/dummy"),
   255  	}
   256  	er, cleanup := execroot.Setup(t, existingFiles)
   257  	defer cleanup()
   258  	c.DepsCache.LoadFromDir(er)
   259  	opts := inputprocessor.Options{
   260  		Cmd:        []string{"../bin/clang++", "-o", "test.o", "-MF", "test.d", "-I../include/foo", "-I", "../include/bar", "-c", "../src/test.cpp"},
   261  		WorkingDir: "out",
   262  		ExecRoot:   er,
   263  		Labels:     map[string]string{"type": "compile", "compiler": "clang", "lang": "cpp"},
   264  	}
   265  	var wg sync.WaitGroup
   266  	wg.Add(1)
   267  	c.testOnlySetDone = func() { wg.Done() }
   268  	got, err := inputprocessor.Compute(c, opts)
   269  	if err != nil {
   270  		t.Errorf("Compute() failed: %v", err)
   271  	}
   272  	want := &command.InputSpec{
   273  		Inputs: []string{
   274  			filepath.Clean("src/test.cpp"),
   275  			filepath.Clean("bin/clang++"),
   276  		},
   277  		VirtualInputs: []*command.VirtualInput{
   278  			{Path: filepath.Clean("include/foo"), IsEmptyDirectory: true},
   279  		},
   280  	}
   281  	if diff := cmp.Diff(want, got.InputSpec, strSliceCmp); diff != "" {
   282  		t.Errorf("Compute() returned diff (-want +got): %v", diff)
   283  	}
   284  	wg.Wait()
   285  	c.DepsCache.WriteToDisk(er)
   286  	// Simulate new run of reproxy
   287  	clearResourceDirCache(t)
   288  	c = &Preprocessor{
   289  		CPPDepScanner: s,
   290  		BasePreprocessor: &inputprocessor.BasePreprocessor{
   291  			Ctx:               ctx,
   292  			FileMetadataCache: fmc,
   293  			Executor:          &stubExecutor{outStr: "/second/resource/dir"},
   294  		},
   295  		DepsCache: depscache.New(filemetadata.NewSingleFlightCache()),
   296  	}
   297  	c.DepsCache.LoadFromDir(er)
   298  	got, err = inputprocessor.Compute(c, opts)
   299  	if err != nil {
   300  		t.Errorf("Compute() failed: %v", err)
   301  	}
   302  	if diff := cmp.Diff(want, got.InputSpec, strSliceCmp); diff != "" {
   303  		t.Errorf("Compute() returned diff (-want +got): %v", diff)
   304  	}
   305  	if s.processCalls != 2 {
   306  		t.Errorf("Wrong number of ProcessInputs calls: got %v, want 1", s.processCalls)
   307  	}
   308  }
   309  
   310  func clearResourceDirCache(t *testing.T) {
   311  	t.Helper()
   312  	resourceDirsMu.Lock()
   313  	defer resourceDirsMu.Unlock()
   314  	resourceDirs = map[string]resourceDirInfo{}
   315  }
   316  
   317  func TestComputeSpec_SysrootAndProfileSampleUseArgsConvertedToAbsolutePath(t *testing.T) {
   318  	ctx := context.Background()
   319  	fmc := filemetadata.NewSingleFlightCache()
   320  	s := &stubCPPDepScanner{
   321  		res: []string{"include/foo.h"},
   322  		err: nil,
   323  	}
   324  	c := &Preprocessor{CPPDepScanner: s, BasePreprocessor: &inputprocessor.BasePreprocessor{Ctx: ctx, FileMetadataCache: fmc}}
   325  
   326  	pwd, err := os.Getwd()
   327  	if err != nil {
   328  		t.Fatalf("Unable to get current working directory: %v", err)
   329  	}
   330  	c.Flags = &flags.CommandFlags{
   331  		ExecutablePath:   "../bin/clang++",
   332  		TargetFilePaths:  []string{"../src/test.cpp"},
   333  		OutputFilePaths:  []string{"test.o"},
   334  		ExecRoot:         pwd,
   335  		WorkingDirectory: "out",
   336  		Flags: []*flags.Flag{
   337  			{Key: "-c"},
   338  			{Key: "--sysroot", Value: "a/b", Joined: false},
   339  			{Key: "-isysroot", Value: "../c/d", Joined: true},
   340  			{Key: "--sysroot=", Value: fmt.Sprintf("%s/e/f", pwd), Joined: true},
   341  			{Key: "-fprofile-sample-use=", Value: "../c/d/abc.prof", Joined: true},
   342  			{Key: "-fsanitize-blacklist=", Value: fmt.Sprintf("%s/e/f/ignores.txt", pwd), Joined: true},
   343  			{Key: "-fsanitize-ignorelist=", Value: fmt.Sprintf("%s/e/f/ignores2.txt", pwd), Joined: true},
   344  			{Key: "-fprofile-list=", Value: fmt.Sprintf("%s/e/f/fun.list", pwd), Joined: true},
   345  		},
   346  	}
   347  	if err := c.ComputeSpec(); err != nil {
   348  		t.Fatalf("ComputeSpec() failed: %v", err)
   349  	}
   350  
   351  	wantCmd := []string{
   352  		filepath.Join(pwd, "bin/clang++"),
   353  		"-c",
   354  		"--sysroot", filepath.Join(pwd, "out/a/b"),
   355  		"-isysroot" + filepath.Join(pwd, "c/d"),
   356  		"--sysroot=" + filepath.Join(pwd, "e/f"),
   357  		"-fprofile-sample-use=" + filepath.Join(pwd, "c/d/abc.prof"),
   358  		"-fsanitize-blacklist=" + filepath.Join(pwd, "e/f/ignores.txt"),
   359  		"-fsanitize-ignorelist=" + filepath.Join(pwd, "e/f/ignores2.txt"),
   360  		"-fprofile-list=" + filepath.Join(pwd, "e/f/fun.list"),
   361  		"-Qunused-arguments",
   362  		"-o",
   363  		"test.o",
   364  		filepath.Join(pwd, "src/test.cpp"),
   365  	}
   366  	if diff := cmp.Diff(wantCmd, s.gotCmd); diff != "" {
   367  		t.Errorf("ComputeSpec() called clang-scan-deps incorrectly, diff (-want +got): %v", diff)
   368  	}
   369  }
   370  
   371  func TestComputeSpecAbsolutePaths(t *testing.T) {
   372  	ctx := context.Background()
   373  	fmc := filemetadata.NewSingleFlightCache()
   374  	s := &stubCPPDepScanner{
   375  		res: []string{"include/foo.h"},
   376  		err: nil,
   377  	}
   378  	c := &Preprocessor{CPPDepScanner: s, BasePreprocessor: &inputprocessor.BasePreprocessor{Ctx: ctx, FileMetadataCache: fmc}}
   379  
   380  	pwd, err := os.Getwd()
   381  	if err != nil {
   382  		t.Fatalf("Unable to get current working directory: %v", err)
   383  	}
   384  	wd := "out"
   385  	c.Flags = &flags.CommandFlags{
   386  		ExecutablePath:   filepath.Join(pwd, "bin/clang++"),
   387  		TargetFilePaths:  []string{filepath.Join(pwd, "src/test.cpp")},
   388  		OutputFilePaths:  []string{filepath.Join(pwd, wd, "test.o")},
   389  		ExecRoot:         pwd,
   390  		WorkingDirectory: wd,
   391  		Flags: []*flags.Flag{
   392  			{Key: "-c"},
   393  			{Key: "--sysroot", Value: filepath.Join(pwd, wd, "a/b"), Joined: false},
   394  			{Key: "-isysroot", Value: filepath.Join(pwd, "c/d"), Joined: true},
   395  			{Key: "--sysroot=", Value: filepath.Join(pwd, "e/f"), Joined: true},
   396  			{Key: "-fprofile-sample-use=", Value: filepath.Join(pwd, "c/d/abc.prof"), Joined: true},
   397  			{Key: "-fsanitize-blacklist=", Value: filepath.Join(pwd, "e/f/ignores.txt"), Joined: true},
   398  			{Key: "-fsanitize-ignorelist=", Value: filepath.Join(pwd, "e/f/ignores2.txt"), Joined: true},
   399  			{Key: "-fprofile-list=", Value: filepath.Join(pwd, "e/f/fun.list"), Joined: true},
   400  		},
   401  	}
   402  	if err := c.ComputeSpec(); err != nil {
   403  		t.Fatalf("ComputeSpec() failed: %v", err)
   404  	}
   405  
   406  	wantCmd := []string{
   407  		filepath.Join(pwd, "bin/clang++"),
   408  		"-c",
   409  		"--sysroot", filepath.Join(pwd, "out/a/b"),
   410  		"-isysroot" + filepath.Join(pwd, "c/d"),
   411  		"--sysroot=" + filepath.Join(pwd, "e/f"),
   412  		"-fprofile-sample-use=" + filepath.Join(pwd, "c/d/abc.prof"),
   413  		"-fsanitize-blacklist=" + filepath.Join(pwd, "e/f/ignores.txt"),
   414  		"-fsanitize-ignorelist=" + filepath.Join(pwd, "e/f/ignores2.txt"),
   415  		"-fprofile-list=" + filepath.Join(pwd, "e/f/fun.list"),
   416  		"-Qunused-arguments",
   417  		"-o",
   418  		filepath.Join(pwd, wd, "test.o"),
   419  		filepath.Join(pwd, "src/test.cpp"),
   420  	}
   421  	if diff := cmp.Diff(wantCmd, s.gotCmd); diff != "" {
   422  		t.Errorf("ComputeSpec() called clang-scan-deps incorrectly, diff (-want +got): %v", diff)
   423  	}
   424  	if s.gotFileName != filepath.Join(pwd, "src/test.cpp") {
   425  		t.Errorf("ComputeSpec() called the input processor incorrectly, want filename=%q, got %q", filepath.Join(pwd, "src/test.cpp"), s.gotFileName)
   426  	}
   427  }
   428  
   429  func TestComputeSpec_RemovesUnsupportedFlags(t *testing.T) {
   430  	ctx := context.Background()
   431  	fmc := filemetadata.NewSingleFlightCache()
   432  	s := &stubCPPDepScanner{
   433  		res: []string{"include/foo.h"},
   434  		err: nil,
   435  	}
   436  	c := &Preprocessor{CPPDepScanner: s, BasePreprocessor: &inputprocessor.BasePreprocessor{Ctx: ctx, FileMetadataCache: fmc}}
   437  
   438  	// Tests that virtual inputs only include existing directories and excludes files.
   439  	existingFiles := []string{
   440  		filepath.Clean("bin/clang++"),
   441  		filepath.Clean("src/test.cpp"),
   442  		filepath.Clean("include/foo/a"),
   443  		filepath.Clean("include/a/b.hmap"),
   444  		filepath.Clean("out/dummy"),
   445  	}
   446  	er, cleanup := execroot.Setup(t, existingFiles)
   447  	defer cleanup()
   448  	inputs := []string{filepath.Clean("include/a/b.hmap")}
   449  	opts := inputprocessor.Options{
   450  		Cmd: []string{"../bin/clang++", "-o", "test.o", "-MF", "test.d", "-I../include/foo", "-I../include/bar", "-I../include/a/b.hmap",
   451  			"-fno-experimental-new-pass-manager", "-fexperimental-new-pass-manager", "-std=c++14", "-Xclang", "-verify", "-c", "../src/test.cpp"},
   452  		WorkingDir: "out",
   453  		ExecRoot:   er,
   454  		Labels:     map[string]string{"type": "compile", "compiler": "clang", "lang": "cpp"},
   455  		Inputs: &command.InputSpec{
   456  			Inputs: inputs,
   457  		},
   458  	}
   459  	if _, err := inputprocessor.Compute(c, opts); err != nil {
   460  		t.Errorf("Spec() failed: %v", err)
   461  	}
   462  	wantCmd := []string{
   463  		filepath.Join(er, "bin/clang++"),
   464  		"-I../include/foo",
   465  		"-I../include/bar",
   466  		"-I../include/a/b.hmap",
   467  		"-std=c++14", "-c", // expect -std=xx to not be normalized
   468  		"-Qunused-arguments", // expect Qunused-arguments to be added, and -Xclang -verify to be removed
   469  		// -fno-experimental-new-pass-manager and -fexperimental-new-pass-manager are removed since
   470  		// they're not supported in newer clang versions.
   471  		"-o", "test.o",
   472  		filepath.Join(er, "src/test.cpp"),
   473  	}
   474  	if diff := cmp.Diff(wantCmd, s.gotCmd); diff != "" {
   475  		t.Errorf("Unexpected command passed to the dependency scanner (-want +got): %v", diff)
   476  	}
   477  }
   478  
   479  func TestBuildCommandLine(t *testing.T) {
   480  	f := &flags.CommandFlags{
   481  		ExecutablePath:        "clang++",
   482  		Flags:                 []*flags.Flag{{Key: "-c"}, {Key: "-Xclang", Value: "-verify"}, {Key: "-Xclang", Value: "-fallow-half-arguments-and-returns"}, {Key: "--sysroot=", Value: "a/b", Joined: true}},
   483  		TargetFilePaths:       []string{"test.cpp"},
   484  		OutputFilePaths:       []string{"test.o", "test.d"},
   485  		EmittedDependencyFile: "test.d",
   486  		WorkingDirectory:      "foo",
   487  		ExecRoot:              "/exec/root",
   488  	}
   489  	p := &Preprocessor{
   490  		BasePreprocessor: &inputprocessor.BasePreprocessor{Flags: f},
   491  		CPPDepScanner:    &stubCPPDepScanner{},
   492  	}
   493  	got := p.BuildCommandLine("-o", false, map[string]bool{"--sysroot=": true})
   494  	want := []string{filepath.Clean("/exec/root/foo/clang++"), "-c", "--sysroot=" + filepath.Clean("/exec/root/foo/a/b"), "-Qunused-arguments", "-o", "test.o", filepath.Clean("/exec/root/foo/test.cpp")}
   495  	if diff := cmp.Diff(want, got); diff != "" {
   496  		t.Errorf("BuildCommandLine returned diff, (-want +got): %s", diff)
   497  	}
   498  }
   499  
   500  // TestVirtualInputFlags checks that all the expected flags that can possibly add virtual
   501  // inputs actually do.
   502  func TestVirtualInputFlags(t *testing.T) {
   503  	pwd, err := os.Getwd()
   504  	if err != nil {
   505  		t.Fatalf("Unable to get current working directory: %v", err)
   506  	}
   507  	f := &flags.CommandFlags{
   508  		ExecRoot:         pwd,
   509  		WorkingDirectory: "out",
   510  		Flags: []*flags.Flag{
   511  			{Value: "-c"},
   512  			// These flags should result in virtual inputs.
   513  			{Key: "--sysroot", Value: "a/b"},
   514  			{Key: "-isysroot", Value: "../c/d", Joined: true},
   515  			{Key: "--sysroot=", Value: fmt.Sprintf("%s/e/f", pwd), Joined: true},
   516  			{Key: "-I", Value: "g/h", Joined: true},
   517  			{Key: "-I", Value: "i/j", Joined: true},
   518  			{Key: "-isysroot", Value: "../foo", Joined: true},
   519  			{Key: "-isystem", Value: "../goo", Joined: true},
   520  			{Key: "-internal-isystem", Value: "../doo", Joined: true},
   521  			{Key: "-internal-externc-isystem", Value: "../loo", Joined: true},
   522  			// These flags should not result in virtual inputs.
   523  			{Key: "-sysroot", Value: "../bar"},
   524  			{Key: "-fprofile-sample-use=", Value: "../c/d/abc.prof", Joined: true},
   525  			{Key: "-fsanitize-blacklist=", Value: fmt.Sprintf("%s/e/f/ignores.txt", pwd), Joined: true},
   526  			{Key: "-fsanitize-ignorelist=", Value: fmt.Sprintf("%s/e/f/ignores.txt", pwd), Joined: true},
   527  		},
   528  	}
   529  	vi := VirtualInputs(f, &Preprocessor{})
   530  
   531  	want := []*command.VirtualInput{
   532  		{Path: filepath.Clean("out/a/b"), IsEmptyDirectory: true},
   533  		{Path: filepath.Clean("c/d"), IsEmptyDirectory: true},
   534  		{Path: filepath.Clean("e/f"), IsEmptyDirectory: true},
   535  		{Path: filepath.Clean("out/g/h"), IsEmptyDirectory: true},
   536  		{Path: filepath.Clean("out/i/j"), IsEmptyDirectory: true},
   537  		{Path: filepath.Clean("foo"), IsEmptyDirectory: true},
   538  		{Path: filepath.Clean("goo"), IsEmptyDirectory: true},
   539  		{Path: filepath.Clean("doo"), IsEmptyDirectory: true},
   540  		{Path: filepath.Clean("loo"), IsEmptyDirectory: true},
   541  	}
   542  	if diff := cmp.Diff(want, vi); diff != "" {
   543  		t.Errorf("virtualInputs(%+v) returned incorrect result diff (-want +got): %v", f, diff)
   544  	}
   545  }
   546  
   547  // TestExtractVirtualSubdirectories checks that paths are translated into virtual inputs correctly.
   548  func TestExtractVirtualSubdirectories(t *testing.T) {
   549  	tests := []struct {
   550  		path string
   551  		want []string
   552  	}{
   553  		{
   554  			path: filepath.FromSlash("a/b/c"),
   555  			want: []string{filepath.FromSlash("a/b/c")},
   556  		},
   557  		{
   558  			path: filepath.FromSlash("../a/b/c"),
   559  			want: []string{filepath.FromSlash("../a/b/c")},
   560  		},
   561  		{
   562  			path: filepath.FromSlash("a/b/../c"),
   563  			want: []string{filepath.FromSlash("a/b"), filepath.FromSlash("a/c")},
   564  		},
   565  		{
   566  			path: filepath.FromSlash("a/b/../c/../d"),
   567  			want: []string{filepath.FromSlash("a/b"), filepath.FromSlash("a/c"), filepath.FromSlash("a/d")},
   568  		}, {
   569  			path: filepath.FromSlash("a/b/../c/d/../../../e/f"),
   570  			want: []string{filepath.FromSlash("a/b"), filepath.FromSlash("a/c/d"), filepath.FromSlash("e/f")},
   571  		}, {
   572  			path: filepath.FromSlash("a/b/c/../.."),
   573  			want: []string{filepath.FromSlash("a/b/c")},
   574  		}, {
   575  			path: filepath.FromSlash("../../.."),
   576  			want: []string{filepath.FromSlash("../../..")},
   577  		},
   578  	}
   579  
   580  	for _, test := range tests {
   581  		t.Run(test.path, func(t *testing.T) {
   582  			vi := extractVirtualSubdirectories(test.path)
   583  
   584  			if diff := cmp.Diff(test.want, vi); diff != "" {
   585  				t.Errorf("extractVirtualSubdirectories(%+v) returned incorrect result diff (-want +got): %v", test.path, diff)
   586  			}
   587  		})
   588  	}
   589  }
   590  
   591  // TestComputeSpecEventTimes tests that the Event Times "InputProcessorCacheLookup",
   592  // "CPPInputProcessor", and "InputProcessorWait" are properly setup when ComputeSpec() is called.
   593  // This was based on TestComputeSpecWithDepsCache and modified to check for Event Times.
   594  func TestComputeSpecEventTimes(t *testing.T) {
   595  	ctx := context.Background()
   596  	fmc := filemetadata.NewSingleFlightCache()
   597  	s := &stubCPPDepScanner{
   598  		res:        []string{"include/foo.h"},
   599  		err:        nil,
   600  		cacheInput: false,
   601  	}
   602  	c := &Preprocessor{
   603  		CPPDepScanner:    s,
   604  		BasePreprocessor: &inputprocessor.BasePreprocessor{Ctx: ctx, FileMetadataCache: fmc},
   605  		DepsCache:        depscache.New(filemetadata.NewSingleFlightCache()),
   606  	}
   607  
   608  	existingFiles := []string{
   609  		filepath.Clean("bin/clang++"),
   610  		filepath.Clean("src/test.cpp"),
   611  		filepath.Clean("include/foo/a"),
   612  		filepath.Clean("out/dummy"),
   613  	}
   614  	er, cleanup := execroot.Setup(t, existingFiles)
   615  	defer cleanup()
   616  	c.DepsCache.LoadFromDir(er)
   617  	opts := inputprocessor.Options{
   618  		Cmd:        []string{"../bin/clang++", "-o", "test.o", "-MF", "test.d", "-I../include/foo", "-I", "../include/bar", "-c", "../src/test.cpp"},
   619  		WorkingDir: "out",
   620  		ExecRoot:   er,
   621  		Labels:     map[string]string{"type": "compile", "compiler": "clang", "lang": "cpp"},
   622  	}
   623  
   624  	// No deps cache hit
   625  	var wg sync.WaitGroup
   626  	wg.Add(1)
   627  	c.testOnlySetDone = func() { wg.Done() }
   628  	inputprocessor.Compute(c, opts)
   629  	wg.Wait()
   630  	if _, okIPCL := c.Rec.GetLocalMetadata().GetEventTimes()["InputProcessorCacheLookup"]; okIPCL {
   631  		t.Errorf("Event Time %s was set but DepsCache was not used", "InputProcessorCacheLookup")
   632  	}
   633  	if _, okIPW := c.Rec.GetLocalMetadata().GetEventTimes()["InputProcessorWait"]; !okIPW {
   634  		t.Errorf("Event Time %s was not set correctly", "InputProcessorWait")
   635  	}
   636  	if _, okCIP := c.Rec.GetLocalMetadata().GetEventTimes()["CPPInputProcessor"]; !okCIP {
   637  		t.Errorf("Event Time %s was not set but DepsCache was not used", "CPPInputProcessor")
   638  	}
   639  	// Cache used but no early deps cache hit
   640  	s.cacheInput = true
   641  	c = &Preprocessor{
   642  		CPPDepScanner:    s,
   643  		BasePreprocessor: &inputprocessor.BasePreprocessor{Ctx: ctx, FileMetadataCache: fmc},
   644  		DepsCache:        depscache.New(filemetadata.NewSingleFlightCache()),
   645  	}
   646  	c.DepsCache.LoadFromDir(er)
   647  	wg.Add(1)
   648  	c.testOnlySetDone = func() { wg.Done() }
   649  	inputprocessor.Compute(c, opts)
   650  	wg.Wait()
   651  	if _, okIPCL := c.Rec.GetLocalMetadata().GetEventTimes()["InputProcessorCacheLookup"]; !okIPCL {
   652  		t.Errorf("Event Time %s was not set but DepsCache was used", "InputProcessorCacheLookup")
   653  	}
   654  	if _, okIPW := c.Rec.GetLocalMetadata().GetEventTimes()["InputProcessorWait"]; !okIPW {
   655  		t.Errorf("Event Time %s was not set correctly", "InputProcessorWait")
   656  	}
   657  	if _, okCIP := c.Rec.GetLocalMetadata().GetEventTimes()["CPPInputProcessor"]; okCIP {
   658  		t.Errorf("Event Time %s was set but DepsCache was used", "CPPInputProcessor")
   659  	}
   660  	// Early deps cache hit
   661  	s.cacheInput = false
   662  	c = &Preprocessor{
   663  		CPPDepScanner:    s,
   664  		BasePreprocessor: &inputprocessor.BasePreprocessor{Ctx: ctx, FileMetadataCache: fmc},
   665  		DepsCache:        c.DepsCache,
   666  	}
   667  	inputprocessor.Compute(c, opts)
   668  	if _, okIPCL := c.Rec.GetLocalMetadata().GetEventTimes()["InputProcessorCacheLookup"]; !okIPCL {
   669  		t.Errorf("Event Time for %s was not set but DepsCache was used", "InputProcessorCacheLookup")
   670  	}
   671  	if _, okIPW := c.Rec.GetLocalMetadata().GetEventTimes()["InputProcessorWait"]; okIPW {
   672  		t.Errorf("Event Time %s was set but an early Deps Cache hit was found", "InputProcessorWait")
   673  	}
   674  	if _, okCIP := c.Rec.GetLocalMetadata().GetEventTimes()["CPPInputProcessor"]; okCIP {
   675  		t.Errorf("Event Time %s was set but an early Deps Cache hit was found", "CPPInputProcessor")
   676  	}
   677  }
   678  
   679  type stubCPPDepScanner struct {
   680  	gotCmd       []string
   681  	gotFileName  string
   682  	gotDirectory string
   683  
   684  	res []string
   685  	err error
   686  
   687  	processCalls  int
   688  	minimizeCalls int
   689  
   690  	capabilities *spb.CapabilitiesResponse
   691  
   692  	cacheInput bool
   693  }
   694  
   695  func (s *stubCPPDepScanner) ProcessInputs(_ context.Context, _ string, command []string, filename, directory string, _ []string) ([]string, bool, error) {
   696  	s.gotCmd = command
   697  	s.gotFileName = filename
   698  	s.gotDirectory = directory
   699  	s.processCalls++
   700  
   701  	return s.res, s.cacheInput, s.err
   702  }
   703  
   704  func (s *stubCPPDepScanner) Capabilities() *spb.CapabilitiesResponse {
   705  	return s.capabilities
   706  }
   707  
   708  type stubExecutor struct {
   709  	gotCmd *command.Command
   710  
   711  	outStr string
   712  	errStr string
   713  	err    error
   714  }
   715  
   716  func (s *stubExecutor) Execute(ctx context.Context, cmd *command.Command) (string, string, error) {
   717  	s.gotCmd = cmd
   718  	return s.outStr, s.errStr, s.err
   719  }