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 }