github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/internal/pkg/inputprocessor/action/clangcl/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 clangcl
    16  
    17  import (
    18  	"context"
    19  	"os"
    20  	"path/filepath"
    21  	"runtime"
    22  	"testing"
    23  
    24  	spb "github.com/bazelbuild/reclient/api/scandeps"
    25  	"github.com/bazelbuild/reclient/internal/pkg/execroot"
    26  	"github.com/bazelbuild/reclient/internal/pkg/inputprocessor"
    27  	"github.com/bazelbuild/reclient/internal/pkg/inputprocessor/action/cppcompile"
    28  
    29  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/command"
    30  	"github.com/google/go-cmp/cmp"
    31  )
    32  
    33  const executablePath = "clang-cl"
    34  
    35  type spyExecutor struct {
    36  	gotCmd         *command.Command
    37  	stdout, stderr string
    38  	err            error
    39  }
    40  
    41  func (e *spyExecutor) Execute(ctx context.Context, cmd *command.Command) (string, string, error) {
    42  	e.gotCmd = cmd
    43  	return e.stdout, e.stderr, e.err
    44  }
    45  
    46  func TestWinSdkDir(t *testing.T) {
    47  	tests := []struct {
    48  		name        string
    49  		dirs        []string
    50  		expectedDir string
    51  		expectedErr bool
    52  	}{
    53  		{
    54  			name: "multiple versions",
    55  			dirs: []string{
    56  				filepath.Join("Windows Kits", "10", "Include", "10.0.20348.0"),
    57  				filepath.Join("Windows Kits", "10", "Include", "10.0.20348.1"),
    58  				filepath.Join("Windows Kits", "5", "Include", "10.0.20348.0"),
    59  				filepath.Join("Windows Kits", "5", "Include", "9.0.20348.0"),
    60  			},
    61  			expectedDir: filepath.Join("Windows Kits", "10", "Include", "10.0.20348.1"),
    62  		},
    63  		{
    64  			name:        "missing dir",
    65  			expectedErr: true,
    66  		},
    67  	}
    68  	for _, test := range tests {
    69  		t.Run(test.name, func(t *testing.T) {
    70  			execRoot := t.TempDir()
    71  			for _, dir := range test.dirs {
    72  				os.MkdirAll(filepath.Join(execRoot, dir), 0755)
    73  			}
    74  			got, err := winSDKDir(execRoot)
    75  			if err != nil && !test.expectedErr {
    76  				t.Errorf("Got error = %v, expected none", err)
    77  			}
    78  			if err == nil && test.expectedErr {
    79  				t.Errorf("Expected error, got none")
    80  			}
    81  			got, _ = filepath.Rel(execRoot, got)
    82  			if got != test.expectedDir {
    83  				t.Errorf("Expected dir = %q, got = %q", test.expectedDir, got)
    84  			}
    85  		})
    86  	}
    87  }
    88  
    89  func TestResourceDir(t *testing.T) {
    90  	ctx := context.Background()
    91  	execRoot := t.TempDir()
    92  
    93  	var clangCL, want, newline string
    94  	if runtime.GOOS == "windows" {
    95  		clangCL = filepath.Join(execRoot, `third_party\llvm-build\Release+Asserts\bin\clang-cl.exe`)
    96  		want = filepath.Join(execRoot, `third_party\llvm-build\Release+Asserts\lib\clang\12.0.0`)
    97  		newline = "\r\n"
    98  	} else {
    99  		clangCL = filepath.Join(execRoot, "third_party/llvm-build/Release+Asserts/bin/clang-cl")
   100  		want = filepath.Join(execRoot, "third_party/llvm-build/Release+Asserts/lib/clang/12.0.0")
   101  		newline = "\n"
   102  	}
   103  	err := os.MkdirAll(filepath.Dir(clangCL), 0755)
   104  	if err != nil {
   105  		t.Fatal(err)
   106  	}
   107  	err = os.WriteFile(clangCL, nil, 0755)
   108  	if err != nil {
   109  		t.Fatal(err)
   110  	}
   111  
   112  	e := &spyExecutor{
   113  		stdout: `clang version 12.0.0 (https://github.com/llvm/llvm-project/ f086e85eea94a51eb42115496ac5d24f07bc8791)` + newline +
   114  			`Target: x86_64-pc-windows-msvc` + newline +
   115  			`Thread model: posix` + newline +
   116  			`InstalledDir: ` + filepath.Dir(clangCL) + newline,
   117  	}
   118  	p := &Preprocessor{
   119  		Preprocessor: &cppcompile.Preprocessor{
   120  			BasePreprocessor: &inputprocessor.BasePreprocessor{
   121  				Ctx:      ctx,
   122  				Executor: e,
   123  			},
   124  		},
   125  	}
   126  
   127  	got := p.resourceDir([]string{clangCL, "/showIncludes:user", "/c", "../../base/foo.cc", "/Foobj/base/base/foo.obj"})
   128  	if got != want {
   129  		t.Errorf("p.resourceDir([]string{%q, ..})=%q; want=%q", clangCL, got, want)
   130  	}
   131  	wantCmd := &command.Command{
   132  		Args:       []string{clangCL, "--version"},
   133  		WorkingDir: "/",
   134  	}
   135  	if !cmp.Equal(e.gotCmd, wantCmd) {
   136  		t.Errorf("executor got=%v; want=%v", e.gotCmd, wantCmd)
   137  	}
   138  }
   139  
   140  func TestComputeSpec(t *testing.T) {
   141  	tests := []struct {
   142  		name                 string
   143  		scandepsCapabilities *spb.CapabilitiesResponse
   144  		versionOutput        string
   145  		inputExtraCmd        []string
   146  		wantResDir           string
   147  		wantResDirRelToEr    bool
   148  	}{
   149  		{
   150  			name:                 "ResourceDirProvided/ExpectsResourceDir",
   151  			scandepsCapabilities: &spb.CapabilitiesResponse{ExpectsResourceDir: true},
   152  			versionOutput:        "clang version 16.0.6 (16)",
   153  			inputExtraCmd:        []string{"-resource-dir", "/some/dir"},
   154  			wantResDir:           "/some/dir",
   155  		},
   156  		{
   157  			name:                 "ResourceDirProvided/DoesntExpectResourceDir",
   158  			scandepsCapabilities: &spb.CapabilitiesResponse{ExpectsResourceDir: false},
   159  			versionOutput:        "clang version 16.0.6 (16)",
   160  			inputExtraCmd:        []string{"-resource-dir", "/some/dir"},
   161  			wantResDir:           "/some/dir",
   162  		},
   163  		{
   164  			name:                 "ResourceDirMissing/ExpectsResourceDir",
   165  			scandepsCapabilities: &spb.CapabilitiesResponse{ExpectsResourceDir: true},
   166  			versionOutput:        "clang version 16.0.6 (16)",
   167  			wantResDir:           "lib/clang/16.0.6",
   168  			wantResDirRelToEr:    true,
   169  		},
   170  		{
   171  			name:                 "ResourceDirMissing/DoesntExpectResourceDir",
   172  			scandepsCapabilities: &spb.CapabilitiesResponse{ExpectsResourceDir: false},
   173  			versionOutput:        "clang version 16.0.6 (16)",
   174  		},
   175  	}
   176  	for _, tc := range tests {
   177  		tc := tc
   178  		t.Run(tc.name, func(t *testing.T) {
   179  			ctx := context.Background()
   180  			s := &stubCPPDepScanner{
   181  				res:          []string{"foo.h"},
   182  				err:          nil,
   183  				capabilities: tc.scandepsCapabilities,
   184  			}
   185  
   186  			// Tests that virtual inputs only include existing directories and excludes files.
   187  			existingFiles := []string{
   188  				filepath.Clean("bin/clang-cl"),
   189  				filepath.Clean("out/dummy"),
   190  			}
   191  			er, cleanup := execroot.Setup(t, existingFiles)
   192  			t.Cleanup(cleanup)
   193  			os.MkdirAll(filepath.Join(er, "Windows Kits", "10", "Include", "10.0.20348.0"), 0755)
   194  			os.MkdirAll(filepath.Join(er, "VC", "Tools", "MSVC", "14.29.30133"), 0755)
   195  			p := Preprocessor{
   196  				Preprocessor: &cppcompile.Preprocessor{
   197  					BasePreprocessor: &inputprocessor.BasePreprocessor{
   198  						Ctx:      ctx,
   199  						Executor: &stubExecutor{outStr: tc.versionOutput},
   200  					},
   201  					CPPDepScanner: s,
   202  				},
   203  			}
   204  			p.Options = inputprocessor.Options{
   205  				ExecRoot:   er,
   206  				WorkingDir: "out",
   207  				Cmd: append(
   208  					[]string{filepath.Join(er, "bin/clang-cl"),
   209  						"-header-filter=\"(packages)\"",
   210  						"-extra-arg-before=-Xclang",
   211  						"test.cpp",
   212  						"--",
   213  						// These flags should result in virtual inputs.
   214  						"/I", "a/b",
   215  						"/I../c/d",
   216  						"-I", "g/h",
   217  						"-Ii/j",
   218  						"-imsvc", "../foo",
   219  						"/winsysroot", er,
   220  						// These flags should not result in virtual inputs.
   221  						"-sysroot../bar",
   222  						"--sysroot../baz",
   223  						"-fprofile-sample-use=../c/d/abc.prof",
   224  						// -Xclang -verify should be removed from output
   225  						"-Xclang",
   226  						"-verify",
   227  						"-c",
   228  						"/Fotest.o",
   229  						"-o", "test.d",
   230  					}, tc.inputExtraCmd...),
   231  			}
   232  			if err := p.ParseFlags(); err != nil {
   233  				t.Fatalf("ParseFlags() failed: %v", err)
   234  			}
   235  			if err := p.ComputeSpec(); err != nil {
   236  				t.Fatalf("ComputeSpec() failed: %v", err)
   237  			}
   238  			spec, _ := p.Spec()
   239  			// expect files specified both by -o and /Fo
   240  			if diff := cmp.Diff(spec.OutputFiles, []string{filepath.Join("out", "test.o"), filepath.Join("out", "test.d")}); diff != "" {
   241  				t.Errorf("OutputFiles has diff (-want +got): %s", diff)
   242  			}
   243  
   244  			wantVirtualOutputs := []*command.VirtualInput{
   245  				{Path: filepath.Clean(filepath.Join("out", "a/b")), IsEmptyDirectory: true},
   246  				{Path: filepath.Clean("c/d"), IsEmptyDirectory: true},
   247  				{Path: filepath.Clean(filepath.Join("out", "g/h")), IsEmptyDirectory: true},
   248  				{Path: filepath.Clean(filepath.Join("out", "i/j")), IsEmptyDirectory: true},
   249  				{Path: filepath.Clean("foo"), IsEmptyDirectory: true},
   250  				{Path: filepath.Clean(filepath.Join("Windows Kits", "10", "Include", "10.0.20348.0")), IsEmptyDirectory: true},
   251  				{Path: filepath.Clean(filepath.Join("VC", "Tools", "MSVC", "14.29.30133")), IsEmptyDirectory: true},
   252  			}
   253  			if diff := cmp.Diff(wantVirtualOutputs, spec.InputSpec.VirtualInputs); diff != "" {
   254  				t.Errorf("InputSpec.VirtualInputs had diff (-want +got): %v", diff)
   255  			}
   256  
   257  			wantCmd := []string{
   258  				filepath.Join(er, "bin/clang-cl"),
   259  				"-header-filter=\"(packages)\"",
   260  				"-extra-arg-before=-Xclang",
   261  				"--",
   262  				"/I", "a/b",
   263  				"/I../c/d",
   264  				"-I", "g/h",
   265  				"-Ii/j",
   266  				"-imsvc", "../foo",
   267  				"/winsysroot", er,
   268  				"-sysroot../bar",
   269  				"--sysroot../baz",
   270  				"-fprofile-sample-use=../c/d/abc.prof",
   271  				"-c",
   272  				"-Qunused-arguments",
   273  				"/Fotest.o",
   274  				"/Fotest.d", // -o normalized to /Fo
   275  				filepath.Join(er, "out", "test.cpp"),
   276  			}
   277  			gotCmdNoResDir := make([]string, 0, len(s.gotCmd))
   278  			i := 0
   279  			gotResDir := ""
   280  			for i < len(s.gotCmd) {
   281  				if s.gotCmd[i] == "-resource-dir" {
   282  					i++
   283  					gotResDir = s.gotCmd[i]
   284  				} else {
   285  					gotCmdNoResDir = append(gotCmdNoResDir, s.gotCmd[i])
   286  				}
   287  				i++
   288  			}
   289  			if diff := cmp.Diff(wantCmd, gotCmdNoResDir); diff != "" {
   290  				t.Errorf("CPP command from %v command %v had diff (-want +got): %s", executablePath, p.Flags, diff)
   291  			}
   292  			wantResDir := tc.wantResDir
   293  			if wantResDir != "" && tc.wantResDirRelToEr {
   294  				wantResDir = filepath.Join(er, wantResDir)
   295  			}
   296  			if wantResDir != gotResDir {
   297  				t.Errorf("CPP command had incorrect resource dir, wanted %v, got %v", wantResDir, gotResDir)
   298  			}
   299  		})
   300  	}
   301  }
   302  
   303  type stubCPPDepScanner struct {
   304  	gotCmd       []string
   305  	gotFileName  string
   306  	gotDirectory string
   307  
   308  	res []string
   309  	err error
   310  
   311  	capabilities *spb.CapabilitiesResponse
   312  
   313  	processCalls  int
   314  	minimizeCalls int
   315  }
   316  
   317  func (s *stubCPPDepScanner) ProcessInputs(_ context.Context, _ string, command []string, filename, directory string, _ []string) ([]string, bool, error) {
   318  	s.gotCmd = command
   319  	s.gotFileName = filename
   320  	s.gotDirectory = directory
   321  	s.processCalls++
   322  
   323  	return s.res, false, s.err
   324  }
   325  
   326  func (s *stubCPPDepScanner) Capabilities() *spb.CapabilitiesResponse {
   327  	return s.capabilities
   328  }
   329  
   330  type stubExecutor struct {
   331  	gotCmd *command.Command
   332  
   333  	outStr string
   334  	errStr string
   335  	err    error
   336  }
   337  
   338  func (s *stubExecutor) Execute(ctx context.Context, cmd *command.Command) (string, string, error) {
   339  	s.gotCmd = cmd
   340  	return s.outStr, s.errStr, s.err
   341  }