github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/command/command_test.go (about)

     1  package command
     2  
     3  import (
     4  	"errors"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/google/go-cmp/cmp"
     9  	"github.com/google/go-cmp/cmp/cmpopts"
    10  	"google.golang.org/protobuf/proto"
    11  
    12  	repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    13  )
    14  
    15  func TestStableId_SameCommands(t *testing.T) {
    16  	t.Parallel()
    17  	testcases := []struct {
    18  		label string
    19  		A, B  *Command
    20  	}{
    21  		{
    22  			label: "platform",
    23  			A: &Command{
    24  				Platform: map[string]string{"a": "1", "b": "2", "c": "3"},
    25  			},
    26  			B: &Command{
    27  				Platform: map[string]string{"c": "3", "b": "2", "a": "1"},
    28  			},
    29  		},
    30  		{
    31  			label: "inputs",
    32  			A: &Command{
    33  				InputSpec: &InputSpec{
    34  					Inputs: []string{"a", "b", "c"},
    35  				},
    36  			},
    37  			B: &Command{
    38  				InputSpec: &InputSpec{
    39  					Inputs: []string{"c", "b", "a"},
    40  				},
    41  			},
    42  		},
    43  		{
    44  			label: "output files",
    45  			A: &Command{
    46  				OutputFiles: []string{"a", "b", "c"},
    47  			},
    48  			B: &Command{
    49  				OutputFiles: []string{"c", "b", "a"},
    50  			},
    51  		},
    52  		{
    53  			label: "output directories",
    54  			A: &Command{
    55  				OutputDirs: []string{"a", "b", "c"},
    56  			},
    57  			B: &Command{
    58  				OutputDirs: []string{"c", "b", "a"},
    59  			},
    60  		},
    61  		{
    62  			label: "environment",
    63  			A: &Command{
    64  				InputSpec: &InputSpec{
    65  					EnvironmentVariables: map[string]string{"a": "1", "b": "2", "c": "3"},
    66  				},
    67  			},
    68  			B: &Command{
    69  				InputSpec: &InputSpec{
    70  					EnvironmentVariables: map[string]string{"c": "3", "b": "2", "a": "1"},
    71  				},
    72  			},
    73  		},
    74  		{
    75  			label: "exclusions",
    76  			A: &Command{
    77  				InputSpec: &InputSpec{
    78  					InputExclusions: []*InputExclusion{
    79  						{Regex: "a", Type: FileInputType},
    80  						{Regex: "b", Type: DirectoryInputType},
    81  						{Regex: "c", Type: UnspecifiedInputType},
    82  					},
    83  				},
    84  			},
    85  			B: &Command{
    86  				InputSpec: &InputSpec{
    87  					InputExclusions: []*InputExclusion{
    88  						{Regex: "b", Type: DirectoryInputType},
    89  						{Regex: "c", Type: UnspecifiedInputType},
    90  						{Regex: "a", Type: FileInputType},
    91  					},
    92  				},
    93  			},
    94  		},
    95  	}
    96  	for _, tc := range testcases {
    97  		aID := tc.A.stableID()
    98  		bID := tc.B.stableID()
    99  		if aID != bID {
   100  			t.Errorf("%s: stableID of %v = %s different from %v = %s", tc.label, tc.A, aID, tc.B, bID)
   101  		}
   102  	}
   103  }
   104  
   105  func TestStableId_DifferentCommands(t *testing.T) {
   106  	t.Parallel()
   107  	testcases := []struct {
   108  		label string
   109  		A, B  *Command
   110  	}{
   111  		{
   112  			label: "args",
   113  			A:     &Command{Args: []string{"a", "b"}},
   114  			B:     &Command{Args: []string{"b", "a"}},
   115  		},
   116  		{
   117  			label: "exec root",
   118  			A:     &Command{ExecRoot: "a"},
   119  			B:     &Command{ExecRoot: "b"},
   120  		},
   121  		{
   122  			label: "working dir",
   123  			A:     &Command{WorkingDir: "a"},
   124  			B:     &Command{WorkingDir: "b"},
   125  		},
   126  		{
   127  			label: "output files",
   128  			A:     &Command{OutputFiles: []string{"a", "b", "c"}},
   129  			B:     &Command{OutputFiles: []string{"c", "b", "c"}},
   130  		},
   131  		{
   132  			label: "output dirs",
   133  			A:     &Command{OutputDirs: []string{"a", "b", "c"}},
   134  			B:     &Command{OutputDirs: []string{"c", "b", "c"}},
   135  		},
   136  		{
   137  			label: "platform",
   138  			A:     &Command{Platform: map[string]string{"a": "1", "b": "2", "c": "3"}},
   139  			B:     &Command{Platform: map[string]string{"c": "3", "b": "2", "a": "10"}},
   140  		},
   141  		{
   142  			label: "inputs",
   143  			A: &Command{
   144  				InputSpec: &InputSpec{
   145  					Inputs: []string{"a", "b", "c"},
   146  				},
   147  			},
   148  			B: &Command{
   149  				InputSpec: &InputSpec{
   150  					Inputs: []string{"c", "b", "a1"},
   151  				},
   152  			},
   153  		},
   154  		{
   155  			label: "environment",
   156  			A: &Command{
   157  				InputSpec: &InputSpec{
   158  					EnvironmentVariables: map[string]string{"a": "1", "b": "2", "c": "3"},
   159  				},
   160  			},
   161  			B: &Command{
   162  				InputSpec: &InputSpec{
   163  					EnvironmentVariables: map[string]string{"c": "3", "b": "2", "a": "10"},
   164  				},
   165  			},
   166  		},
   167  		{
   168  			label: "exclusions",
   169  			A: &Command{
   170  				InputSpec: &InputSpec{
   171  					InputExclusions: []*InputExclusion{
   172  						{Regex: "a", Type: FileInputType},
   173  						{Regex: "b", Type: DirectoryInputType},
   174  						{Regex: "c", Type: UnspecifiedInputType},
   175  					},
   176  				},
   177  			},
   178  			B: &Command{
   179  				InputSpec: &InputSpec{
   180  					InputExclusions: []*InputExclusion{
   181  						{Regex: "b", Type: UnspecifiedInputType},
   182  						{Regex: "c", Type: UnspecifiedInputType},
   183  						{Regex: "a", Type: FileInputType},
   184  					},
   185  				},
   186  			},
   187  		},
   188  	}
   189  	for _, tc := range testcases {
   190  		aID := tc.A.stableID()
   191  		bID := tc.B.stableID()
   192  		if aID == bID {
   193  			t.Errorf("%s: stableID of %v = %s is same as %v", tc.label, tc.A, aID, tc.B)
   194  		}
   195  	}
   196  }
   197  
   198  func TestFillDefaultFieldValues_Empty(t *testing.T) {
   199  	t.Parallel()
   200  	c := &Command{}
   201  	c.FillDefaultFieldValues()
   202  	if c.Identifiers == nil {
   203  		t.Fatal("{}.Identifiers = nil, expected filled")
   204  	}
   205  
   206  	if c.Identifiers.CommandID == "" {
   207  		t.Errorf("did not fill command id for empty command")
   208  	}
   209  	if c.Identifiers.ToolName == "" {
   210  		t.Errorf("did not fill tool name for empty command, expected \"remote-client\"")
   211  	}
   212  	if c.Identifiers.InvocationID == "" {
   213  		t.Errorf("did not generate invocation id for empty command")
   214  	}
   215  	if c.InputSpec == nil {
   216  		t.Errorf("did not generate input spec for empty command")
   217  	}
   218  }
   219  
   220  func TestFillDefaultFieldValues_PreserveExisting(t *testing.T) {
   221  	t.Parallel()
   222  	ids := &Identifiers{
   223  		CommandID:    "bla",
   224  		ToolName:     "foo",
   225  		InvocationID: "bar",
   226  	}
   227  	inputSpec := &InputSpec{}
   228  	c := &Command{InputSpec: inputSpec, Identifiers: ids}
   229  	c.FillDefaultFieldValues()
   230  	if c.Identifiers != ids {
   231  		t.Fatal("command.Identifiers address not preserved")
   232  	}
   233  
   234  	if c.Identifiers.CommandID != "bla" {
   235  		t.Errorf("did not preserve CommandID: got %s, expected bla", c.Identifiers.CommandID)
   236  	}
   237  	if c.Identifiers.ToolName != "foo" {
   238  		t.Errorf("did not preserve CommandID: got %s, expected foo", c.Identifiers.ToolName)
   239  	}
   240  	if c.Identifiers.InvocationID != "bar" {
   241  		t.Errorf("did not preserve CommandID: got %s, expected bar", c.Identifiers.InvocationID)
   242  	}
   243  	if c.InputSpec != inputSpec {
   244  		t.Fatal("command.InputSpec address not preserved")
   245  	}
   246  }
   247  
   248  func TestValidate_Errors(t *testing.T) {
   249  	t.Parallel()
   250  	testcases := []struct {
   251  		label   string
   252  		Command *Command
   253  	}{
   254  		{
   255  			label: "missing args",
   256  			Command: &Command{
   257  				Identifiers: &Identifiers{},
   258  				ExecRoot:    "a",
   259  				InputSpec:   &InputSpec{},
   260  			},
   261  		},
   262  		{
   263  			label: "missing input spec",
   264  			Command: &Command{
   265  				Identifiers: &Identifiers{},
   266  				Args:        []string{"a"},
   267  				ExecRoot:    "a",
   268  			},
   269  		},
   270  		{
   271  			label: "missing exec root",
   272  			Command: &Command{
   273  				Identifiers: &Identifiers{},
   274  				Args:        []string{"a"},
   275  				InputSpec:   &InputSpec{},
   276  			},
   277  		},
   278  		{
   279  			label: "missing identifiers",
   280  			Command: &Command{
   281  				Args:      []string{"a"},
   282  				InputSpec: &InputSpec{},
   283  				ExecRoot:  "a",
   284  			},
   285  		},
   286  		{
   287  			label: "mismatch between local and remote working dir depth",
   288  			Command: &Command{
   289  				Identifiers:      &Identifiers{},
   290  				Args:             []string{"a"},
   291  				ExecRoot:         "a",
   292  				InputSpec:        &InputSpec{},
   293  				WorkingDir:       "foo",
   294  				RemoteWorkingDir: "bar/baz",
   295  			},
   296  		},
   297  	}
   298  	for _, tc := range testcases {
   299  		if err := tc.Command.Validate(); err == nil {
   300  			t.Errorf("%s: expected Validate of %v to error, got nil", tc.label, tc.Command)
   301  		}
   302  	}
   303  }
   304  
   305  func TestValidate_NilSuccess(t *testing.T) {
   306  	t.Parallel()
   307  	var c *Command
   308  	if err := c.Validate(); err != nil {
   309  		t.Errorf("expected Validate of nil = nil, got %v", err)
   310  	}
   311  }
   312  
   313  func TestValidate_Success(t *testing.T) {
   314  	t.Parallel()
   315  	c := &Command{
   316  		Identifiers: &Identifiers{},
   317  		Args:        []string{"a"},
   318  		ExecRoot:    "a",
   319  		InputSpec:   &InputSpec{},
   320  	}
   321  	if err := c.Validate(); err != nil {
   322  		t.Errorf("expected Validate of %v = nil, got %v", c, err)
   323  	}
   324  }
   325  
   326  func TestToREProto(t *testing.T) {
   327  	tests := []struct {
   328  		name    string
   329  		cmd     *Command
   330  		wantCmd *repb.Command
   331  	}{
   332  		{
   333  			name:    "pass args",
   334  			cmd:     &Command{Args: []string{"foo", "bar"}},
   335  			wantCmd: &repb.Command{Arguments: []string{"foo", "bar"}},
   336  		},
   337  		{
   338  			name:    "pass working directory",
   339  			cmd:     &Command{WorkingDir: "a/b"},
   340  			wantCmd: &repb.Command{WorkingDirectory: "a/b"},
   341  		},
   342  		{
   343  			name:    "sort output files",
   344  			cmd:     &Command{OutputFiles: []string{"foo", "bar", "abc"}},
   345  			wantCmd: &repb.Command{OutputFiles: []string{"abc", "bar", "foo"}},
   346  		},
   347  		{
   348  			name:    "sort output directories",
   349  			cmd:     &Command{OutputDirs: []string{"foo", "bar", "abc"}},
   350  			wantCmd: &repb.Command{OutputDirectories: []string{"abc", "bar", "foo"}},
   351  		},
   352  		{
   353  			name: "sort environment variables",
   354  			cmd: &Command{
   355  				InputSpec: &InputSpec{
   356  					EnvironmentVariables: map[string]string{"b": "3", "a": "2", "c": "1"},
   357  				},
   358  			},
   359  			wantCmd: &repb.Command{
   360  				EnvironmentVariables: []*repb.Command_EnvironmentVariable{
   361  					&repb.Command_EnvironmentVariable{Name: "a", Value: "2"},
   362  					&repb.Command_EnvironmentVariable{Name: "b", Value: "3"},
   363  					&repb.Command_EnvironmentVariable{Name: "c", Value: "1"},
   364  				},
   365  			},
   366  		},
   367  		{
   368  			name: "sort platform",
   369  			cmd:  &Command{Platform: map[string]string{"b": "3", "a": "2", "c": "1"}},
   370  			wantCmd: &repb.Command{
   371  				Platform: &repb.Platform{
   372  					Properties: []*repb.Platform_Property{
   373  						&repb.Platform_Property{Name: "a", Value: "2"},
   374  						&repb.Platform_Property{Name: "b", Value: "3"},
   375  						&repb.Platform_Property{Name: "c", Value: "1"},
   376  					},
   377  				},
   378  			},
   379  		},
   380  	}
   381  	for _, tc := range tests {
   382  		t.Run(tc.name, func(t *testing.T) {
   383  			tc.cmd.FillDefaultFieldValues()
   384  			gotCmd := tc.cmd.ToREProto(false)
   385  			if diff := cmp.Diff(tc.wantCmd, gotCmd, cmpopts.EquateEmpty(), cmp.Comparer(proto.Equal)); diff != "" {
   386  				t.Errorf("%s: buildCommand gave result diff (-want +got):\n%s", tc.name, diff)
   387  			}
   388  		})
   389  	}
   390  }
   391  
   392  func TestToREProtoWithOutputPaths(t *testing.T) {
   393  	tests := []struct {
   394  		name    string
   395  		cmd     *Command
   396  		wantCmd *repb.Command
   397  	}{
   398  		{
   399  			name:    "pass args",
   400  			cmd:     &Command{Args: []string{"foo", "bar"}},
   401  			wantCmd: &repb.Command{Arguments: []string{"foo", "bar"}},
   402  		},
   403  		{
   404  			name:    "pass working directory",
   405  			cmd:     &Command{WorkingDir: "a/b"},
   406  			wantCmd: &repb.Command{WorkingDirectory: "a/b"},
   407  		},
   408  		{
   409  			name:    "sort output files",
   410  			cmd:     &Command{OutputFiles: []string{"foo", "bar", "abc"}},
   411  			wantCmd: &repb.Command{OutputPaths: []string{"abc", "bar", "foo"}},
   412  		},
   413  		{
   414  			name:    "sort output directories",
   415  			cmd:     &Command{OutputDirs: []string{"foo", "bar", "abc"}},
   416  			wantCmd: &repb.Command{OutputPaths: []string{"abc", "bar", "foo"}},
   417  		},
   418  		{
   419  			name: "sort environment variables",
   420  			cmd: &Command{
   421  				InputSpec: &InputSpec{
   422  					EnvironmentVariables: map[string]string{"b": "3", "a": "2", "c": "1"},
   423  				},
   424  			},
   425  			wantCmd: &repb.Command{
   426  				EnvironmentVariables: []*repb.Command_EnvironmentVariable{
   427  					&repb.Command_EnvironmentVariable{Name: "a", Value: "2"},
   428  					&repb.Command_EnvironmentVariable{Name: "b", Value: "3"},
   429  					&repb.Command_EnvironmentVariable{Name: "c", Value: "1"},
   430  				},
   431  			},
   432  		},
   433  		{
   434  			name: "sort platform",
   435  			cmd:  &Command{Platform: map[string]string{"b": "3", "a": "2", "c": "1"}},
   436  			wantCmd: &repb.Command{
   437  				Platform: &repb.Platform{
   438  					Properties: []*repb.Platform_Property{
   439  						&repb.Platform_Property{Name: "a", Value: "2"},
   440  						&repb.Platform_Property{Name: "b", Value: "3"},
   441  						&repb.Platform_Property{Name: "c", Value: "1"},
   442  					},
   443  				},
   444  			},
   445  		},
   446  	}
   447  	for _, tc := range tests {
   448  		t.Run(tc.name, func(t *testing.T) {
   449  			tc.cmd.FillDefaultFieldValues()
   450  			gotCmd := tc.cmd.ToREProto(true)
   451  			if diff := cmp.Diff(tc.wantCmd, gotCmd, cmpopts.EquateEmpty(), cmp.Comparer(proto.Equal)); diff != "" {
   452  				t.Errorf("%s: buildCommand gave result diff (-want +got):\n%s", tc.name, diff)
   453  			}
   454  		})
   455  	}
   456  }
   457  
   458  func TestToFromProto(t *testing.T) {
   459  	cmd := &Command{
   460  		Identifiers: &Identifiers{
   461  			CommandID:    "a",
   462  			InvocationID: "b",
   463  			ToolName:     "c",
   464  		},
   465  		Args:     []string{"a", "b", "c"},
   466  		ExecRoot: "/exec/root",
   467  		InputSpec: &InputSpec{
   468  			Inputs: []string{"foo.h", "bar.h"},
   469  			VirtualInputs: []*VirtualInput{
   470  				&VirtualInput{
   471  					Path:         "empty_file",
   472  					IsExecutable: true,
   473  					Mtime:        time.Unix(1711556358, 123456789),
   474  				},
   475  				&VirtualInput{
   476  					Path:             "foo/empty_dir",
   477  					IsEmptyDirectory: true,
   478  					Mtime:            time.Unix(1711556358, 123456789),
   479  				},
   480  				&VirtualInput{
   481  					Path:     "foo/bar",
   482  					Contents: []byte("bar-contents"),
   483  					Mtime:    time.Unix(1711556358, 123456789),
   484  				},
   485  			},
   486  			InputExclusions: []*InputExclusion{
   487  				&InputExclusion{
   488  					Regex: "*.bla",
   489  					Type:  DirectoryInputType,
   490  				},
   491  				&InputExclusion{
   492  					Regex: "*.blo",
   493  					Type:  FileInputType,
   494  				},
   495  			},
   496  			EnvironmentVariables: map[string]string{
   497  				"k":  "v",
   498  				"k1": "v1",
   499  			},
   500  			SymlinkBehavior: ResolveSymlink,
   501  		},
   502  		OutputFiles: []string{"a/b/out"},
   503  	}
   504  	gotCmd := FromProto(ToProto(cmd))
   505  	if diff := cmp.Diff(cmd, gotCmd, cmpopts.EquateEmpty()); diff != "" {
   506  		t.Errorf("FromProto(ToProto()) returned diff in result: (-want +got)\n%s", diff)
   507  	}
   508  }
   509  
   510  func TestResultToFromProto(t *testing.T) {
   511  	res := &Result{
   512  		Status:   CacheHitResultStatus,
   513  		ExitCode: 42,
   514  		Err:      errors.New("message"),
   515  	}
   516  	gotRes := ResultFromProto(ResultToProto(res))
   517  	if diff := cmp.Diff(res, gotRes, cmpopts.IgnoreFields(Result{}, "Err")); diff != "" {
   518  		t.Errorf("ResultFromProto(ResultToProto()) returned diff in result: (-want +got)\n%s", diff)
   519  	}
   520  	if res.Err.Error() != gotRes.Err.Error() {
   521  		t.Errorf("ResultFromProto(ResultToProto()) returned diff in error: want %v, got %v", res.Err, gotRes.Err)
   522  	}
   523  }
   524  
   525  func TestTimeIntervalToFromProto(t *testing.T) {
   526  	ti := &TimeInterval{
   527  		From: time.Now(),
   528  		To:   time.Now(),
   529  	}
   530  	gotTi := TimeIntervalFromProto(TimeIntervalToProto(ti))
   531  	if diff := cmp.Diff(ti, gotTi); diff != "" {
   532  		t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned diff in result: (-want +got)\n%s", diff)
   533  	}
   534  	ti = &TimeInterval{From: time.Now()}
   535  	gotTi = TimeIntervalFromProto(TimeIntervalToProto(ti))
   536  	if diff := cmp.Diff(ti, gotTi); diff != "" {
   537  		t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned diff in result: (-want +got)\n%s", diff)
   538  	}
   539  	ti = &TimeInterval{To: time.Now()}
   540  	gotTi = TimeIntervalFromProto(TimeIntervalToProto(ti))
   541  	if diff := cmp.Diff(ti, gotTi); diff != "" {
   542  		t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned diff in result: (-want +got)\n%s", diff)
   543  	}
   544  	ti = &TimeInterval{}
   545  	gotTi = TimeIntervalFromProto(TimeIntervalToProto(ti))
   546  	if diff := cmp.Diff(ti, gotTi); diff != "" {
   547  		t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned diff in result: (-want +got)\n%s", diff)
   548  	}
   549  	gotTi = TimeIntervalFromProto(TimeIntervalToProto(nil))
   550  	if gotTi != nil {
   551  		t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned %v, wanted nil", gotTi)
   552  	}
   553  }