diff --git a/services/util/util.go b/services/util/util.go index 695831cf..f7d030a3 100644 --- a/services/util/util.go +++ b/services/util/util.go @@ -30,6 +30,7 @@ import ( "syscall" "github.com/go-logr/logr" + "github.com/google/go-cmp/cmp" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -80,6 +81,31 @@ func (f optionfunc) apply(opts *cmdOptions) { f(opts) } +// OptionsEqual returns true if the results of applying both Options are equal +func OptionsEqual(a, b Option) bool { + aCmdOptions := &cmdOptions{} + bCmdOptions := &cmdOptions{} + a.apply(aCmdOptions) + b.apply(bCmdOptions) + + return cmp.Equal(aCmdOptions, bCmdOptions, cmp.AllowUnexported(cmdOptions{})) +} + +// OptionsEqual returns true if the results of applying all elements of both Option slices are equal +func OptionSlicesEqual(a, b []Option) bool { + aCmdOptions := &cmdOptions{} + bCmdOptions := &cmdOptions{} + + for _, opt := range a { + opt.apply(aCmdOptions) + } + for _, opt := range b { + opt.apply(bCmdOptions) + } + + return cmp.Equal(aCmdOptions, bCmdOptions, cmp.AllowUnexported(cmdOptions{})) +} + // FailOnStderr is an option where the command will return an error if any output appears on stderr // regardless of exit code. As we're often parsing the text output of specific commands as root // this is a sanity check we're getting expected output. i.e. ps never returns anything on stderr diff --git a/services/util/util_test.go b/services/util/util_test.go index cbb7b5f9..f36745a4 100644 --- a/services/util/util_test.go +++ b/services/util/util_test.go @@ -337,3 +337,88 @@ func TestIntSliceFlag(t *testing.T) { t.Fatal("didn't get error from bad flag set as we should") } } + +func TestOptionsSliceEqual(t *testing.T) { + for _, tc := range []struct { + optionA []Option + optionB []Option + expectedResult bool + name string + }{ + { + name: "multiple EnvVars", + optionA: []Option{EnvVar("A=A"), EnvVar("B=B")}, + optionB: []Option{EnvVar("A=A"), EnvVar("B=B")}, + expectedResult: true, + }, + { + name: "should be true because FailOnStdErr is idempotent", + optionA: []Option{FailOnStderr(), FailOnStderr()}, + optionB: []Option{FailOnStderr()}, + expectedResult: true, + }, + { + name: "Ordering of idempotent funcs shouldn't matter", + optionA: []Option{StdoutMax(3), EnvVar("A=A")}, + optionB: []Option{EnvVar("A=A"), StdoutMax(3)}, + expectedResult: true, + }, + { + name: "EnvVar ordering does matter", + optionA: []Option{EnvVar("A=A"), EnvVar("B=B")}, + optionB: []Option{EnvVar("B=B"), EnvVar("A=A")}, + expectedResult: false, + }, + { + optionA: []Option{CommandGroup(1)}, + optionB: []Option{CommandUser(2), EnvVar("FOO=BAR")}, + expectedResult: false, + }, + } { + tc := tc + result := OptionSlicesEqual(tc.optionA, tc.optionB) + if result != tc.expectedResult { + t.Fatalf("test %s failed: expected %v, got %v", tc.name, tc.expectedResult, result) + } + } +} + +func TestOptionsEqual(t *testing.T) { + for _, tc := range []struct { + optionA Option + optionB Option + expectedResult bool + }{ + { + optionA: EnvVar(""), + optionB: EnvVar(""), + expectedResult: true, + }, + { + optionA: FailOnStderr(), + optionB: FailOnStderr(), + expectedResult: true, + }, + { + optionA: EnvVar("SANSSHELL_PROXY=sansshell.com"), + optionB: EnvVar("SANSSHELL_PROXY=sansshell.com"), + expectedResult: true, + }, + { + optionA: EnvVar(""), + optionB: EnvVar("FOO=false"), + expectedResult: false, + }, + { + optionA: CommandGroup(1), + optionB: CommandUser(2), + expectedResult: false, + }, + } { + tc := tc + result := OptionsEqual(tc.optionA, tc.optionB) + if result != tc.expectedResult { + t.Fatalf("expected %v, got %v", tc.expectedResult, result) + } + } +}