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  }