github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/internal/pkg/inputprocessor/toolchain/toolchain_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 toolchain 16 17 import ( 18 "context" 19 "io" 20 "os" 21 "path/filepath" 22 "runtime" 23 "strings" 24 "testing" 25 26 "github.com/bazelbuild/remote-apis-sdks/go/pkg/command" 27 "github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata" 28 "github.com/bazelbuild/rules_go/go/tools/bazel" 29 "github.com/google/go-cmp/cmp" 30 ) 31 32 func getCwd(t *testing.T) string { 33 t.Helper() 34 cwd, err := os.Getwd() 35 if err != nil { 36 t.Fatalf("Unable to get current working directory: %v", err) 37 } 38 if runtime.GOOS != "windows" { 39 return cwd 40 } 41 // On windows, runfiles are not available in current directory 42 // by default. 43 // Copy runfiles in temp directory. 44 // https://github.com/bazelbuild/rules_go/issues/2491 45 dir, err := bazel.NewTmpDir("toolchain_test.runfiles") 46 if err != nil { 47 t.Fatalf("tmpdir: %v", err) 48 } 49 t.Cleanup(func() { 50 os.Chdir(cwd) 51 os.RemoveAll(dir) 52 }) 53 err = os.Chdir(dir) 54 if err != nil { 55 t.Fatalf("Unable to chdir to %s: %v", dir, err) 56 } 57 files, err := bazel.ListRunfiles() 58 if err != nil { 59 t.Fatalf("Unable to get runfiles: %v", err) 60 } 61 for _, f := range files { 62 fileCopy(t, f.Path, f.ShortPath) 63 } 64 return filepath.Join(dir, "internal", "pkg", "inputprocessor", "toolchain") 65 } 66 67 func fileCopy(t *testing.T, src, dst string) { 68 t.Helper() 69 // Windows local execution ListRunfiles only shows files, but windows 70 // remote shows files and dirs. Maybe because locally this is invoked 71 // by MSYS64 bash, which bazel uses, while remotely the executable 72 // is run by cmd. 73 stat, err := os.Stat(src) 74 if err != nil { 75 t.Errorf("Couldn't stat %s: %v", src, err) 76 } 77 if !stat.Mode().IsRegular() { 78 return 79 } 80 81 t.Logf("%s -> %s", src, dst) 82 err = os.MkdirAll(filepath.Dir(dst), 0755) 83 if err != nil { 84 t.Fatalf("Unable to mkdir %s: %v", filepath.Dir(dst), err) 85 } 86 r, err := os.Open(src) 87 if err != nil { 88 t.Fatalf("Unable to open runfiles src %s: %v", src, err) 89 } 90 defer r.Close() 91 w, err := os.Create(dst) 92 if err != nil { 93 t.Fatalf("Unable to create runfiles dst %s: %v", dst, err) 94 } 95 _, err = io.Copy(w, r) 96 if err != nil { 97 t.Fatalf("Unable to copy runfiles from %s to %s: %v", src, dst, err) 98 } 99 err = w.Close() 100 if err != nil { 101 t.Fatalf("Unable to close runfiles dst %s: %v", dst, err) 102 } 103 } 104 105 func TestProcessToolchainInputs(t *testing.T) { 106 cwd := getCwd(t) 107 ip := &InputProcessor{} 108 got, err := ip.ProcessToolchainInputs(context.Background(), cwd, ".", "testdata/executable", nil, nil) 109 if err != nil { 110 t.Fatalf("ProcessToolchainInputs() returned error: %v", err) 111 } 112 113 want := &command.InputSpec{ 114 Inputs: []string{filepath.Clean("testdata/executable"), filepath.Clean("testdata/a.txt"), filepath.Clean("testdata/b.txt")}, 115 } 116 if diff := cmp.Diff(want, got); diff != "" { 117 t.Errorf("ProcessToolchainInputs() returned diff. (-want +got)\n%v", diff) 118 } 119 } 120 121 func TestProcessToolchainInputsMultipleToolchains(t *testing.T) { 122 cwd := getCwd(t) 123 fmc := filemetadata.NewSingleFlightCache() 124 ip := &InputProcessor{} 125 got, err := ip.ProcessToolchainInputs(context.Background(), cwd, ".", "testdata/executable3", []string{"testdata/executable2"}, fmc) 126 if err != nil { 127 t.Fatalf("ProcessToolchainInputs() returned error: %v", err) 128 } 129 130 want := &command.InputSpec{ 131 Inputs: []string{filepath.Clean("testdata/executable2"), filepath.Clean("testdata/a.txt"), filepath.Clean("testdata/b.txt"), filepath.Clean("testdata/executable3")}, 132 EnvironmentVariables: map[string]string{"PATH": "testdata:" + strings.Join(defaultPath, ":")}, 133 } 134 if diff := cmp.Diff(want, got); diff != "" { 135 t.Errorf("ProcessToolchainInputs() returned diff. (-want +got)\n%v", diff) 136 } 137 if runtime.GOOS == "windows" { 138 md := fmc.Get(filepath.Join(cwd, "testdata/executable3")) 139 if md.Err != nil || !md.IsExecutable { 140 t.Errorf("fmc.Get(\"testdata/executable3\") = %v, IsExecutable: %v, want no err and IsExecutable=true", md.Err, md.IsExecutable) 141 } 142 md = fmc.Get(filepath.Join(cwd, "testdata/executable2")) 143 if md.Err != nil || !md.IsExecutable { 144 t.Errorf("fmc.Get(\"testdata/executable2\") = %v, IsExecutable: %v, want no err and IsExecutable=true", md.Err, md.IsExecutable) 145 } 146 } 147 } 148 149 func TestProcessToolchainInputsBinInputs(t *testing.T) { 150 cwd := getCwd(t) 151 ip := &InputProcessor{} 152 got, err := ip.ProcessToolchainInputs(context.Background(), cwd, ".", "testdata2/executable", nil, nil) 153 if err != nil { 154 t.Fatalf("ProcessToolchainInputs() returned error: %v", err) 155 } 156 157 want := &command.InputSpec{ 158 Inputs: []string{filepath.Clean("testdata2/executable"), filepath.Clean("testdata2/a.txt"), filepath.Clean("testdata2/b.txt")}, 159 } 160 if diff := cmp.Diff(want, got); diff != "" { 161 t.Errorf("ProcessToolchainInputs() returned diff. (-want +got)\n%v", diff) 162 } 163 } 164 165 // files specified in remote_toolchain_inputs are not in InputSpec.Inputs (no cache) 166 func TestProcessToolchainInputs_RemoteToolchainInputsNotInInputSpec(t *testing.T) { 167 cwd := getCwd(t) 168 ip := &InputProcessor{} 169 got, err := ip.ProcessToolchainInputs(context.Background(), cwd, ".", "testdata/executable", []string{}, nil) 170 if err != nil { 171 t.Fatalf("ProcessToolchainInputs() returned error: %v", err) 172 } 173 174 want := &command.InputSpec{ 175 // only files that exist on disk are expected to be added to Inputs (other files from remote_toolchain_inputs should be ignored) 176 Inputs: []string{filepath.Clean("testdata/executable"), filepath.Clean("testdata/a.txt"), filepath.Clean("testdata/b.txt")}, 177 } 178 if diff := cmp.Diff(want, got); diff != "" { 179 t.Errorf("ProcessToolchainInputs() returned diff. (-want +got)\n%v", diff) 180 } 181 } 182 183 // files specified in remote_toolchain_inputs are not in InputSpec.Inputs (no cache) 184 func TestProcessToolchainInputs_RemoteToolchainInputsNotInInputSpec_Cache(t *testing.T) { 185 cwd := getCwd(t) 186 ip := &InputProcessor{} 187 got, err := ip.ProcessToolchainInputs(context.Background(), cwd, ".", "testdata/executable", []string{}, 188 filemetadata.NewSingleFlightCache()) 189 if err != nil { 190 t.Fatalf("ProcessToolchainInputs() returned error: %v", err) 191 } 192 193 want := &command.InputSpec{ 194 // only files that exist on disk are expected to be added to Inputs (other files from remote_toolchain_inputs should be ignored) 195 Inputs: []string{filepath.Clean("testdata/executable"), filepath.Clean("testdata/a.txt"), filepath.Clean("testdata/b.txt")}, 196 } 197 if diff := cmp.Diff(want, got); diff != "" { 198 t.Errorf("ProcessToolchainInputs() returned diff. (-want +got)\n%v", diff) 199 } 200 } 201 202 func TestProcessToolchainInputs_NestedWorkingDirOneLevel(t *testing.T) { 203 cwd := getCwd(t) 204 ip := &InputProcessor{} 205 got, err := ip.ProcessToolchainInputs(context.Background(), cwd, "build", "testdata/executable", []string{"testdata/executable2"}, nil) 206 if err != nil { 207 t.Fatalf("ProcessToolchainInputs() returned error: %v", err) 208 } 209 210 want := &command.InputSpec{ 211 Inputs: []string{filepath.Clean("testdata/executable2"), filepath.Clean("testdata/a.txt"), filepath.Clean("testdata/b.txt"), filepath.Clean("build/testdata/executable")}, 212 EnvironmentVariables: map[string]string{"PATH": filepath.Join("..", "testdata:") + strings.Join(defaultPath, ":")}, 213 } 214 if diff := cmp.Diff(want, got); diff != "" { 215 t.Errorf("ProcessToolchainInputs() returned diff. (-want +got)\n%v", diff) 216 } 217 } 218 219 func TestProcessToolchainInputs_NestedWorkingDirMultipleLevels(t *testing.T) { 220 cwd := getCwd(t) 221 ip := &InputProcessor{} 222 got, err := ip.ProcessToolchainInputs(context.Background(), cwd, "build/in/here", "testdata/executable", []string{"testdata/executable2"}, nil) 223 if err != nil { 224 t.Fatalf("ProcessToolchainInputs() returned error: %v", err) 225 } 226 227 want := &command.InputSpec{ 228 Inputs: []string{filepath.Clean("testdata/executable2"), filepath.Clean("testdata/a.txt"), filepath.Clean("testdata/b.txt"), filepath.Clean("build/in/here/testdata/executable")}, 229 EnvironmentVariables: map[string]string{"PATH": filepath.Join("..", "..", "..", "testdata:") + strings.Join(defaultPath, ":")}, 230 } 231 if diff := cmp.Diff(want, got); diff != "" { 232 t.Errorf("ProcessToolchainInputs() returned diff. (-want +got)\n%v", diff) 233 } 234 } 235 236 func TestProcessToolchainInputs_NestedWorkingDirAbsPath(t *testing.T) { 237 cwd := getCwd(t) 238 ip := &InputProcessor{} 239 got, err := ip.ProcessToolchainInputs(context.Background(), cwd, "build/in/here", "testdata/executable", []string{cwd + "/testdata/executable2"}, nil) 240 if err != nil { 241 t.Fatalf("ProcessToolchainInputs() returned error: %v", err) 242 } 243 244 want := &command.InputSpec{ 245 Inputs: []string{filepath.Clean("testdata/executable2"), filepath.Clean("testdata/a.txt"), filepath.Clean("testdata/b.txt"), filepath.Clean("build/in/here/testdata/executable")}, 246 EnvironmentVariables: map[string]string{"PATH": filepath.Join("..", "..", "..", "testdata:") + strings.Join(defaultPath, ":")}, 247 } 248 if diff := cmp.Diff(want, got); diff != "" { 249 t.Errorf("ProcessToolchainInputs() returned diff. (-want +got)\n%v", diff) 250 } 251 } 252 253 type execStub struct { 254 execCount int 255 stdout string 256 stderr string 257 err error 258 } 259 260 func (e *execStub) Execute(ctx context.Context, cmd *command.Command) (string, string, error) { 261 e.execCount++ 262 return e.stdout, e.stderr, e.err 263 }