github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cli/args_test.go (about) 1 package cli 2 3 import ( 4 "fmt" 5 "os" 6 "runtime" 7 "strings" 8 "testing" 9 10 "github.com/alessio/shellescape" 11 "github.com/stretchr/testify/require" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/apimachinery/pkg/types" 14 "k8s.io/cli-runtime/pkg/genericclioptions" 15 16 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 17 "github.com/tilt-dev/tilt/pkg/model" 18 "github.com/tilt-dev/wmclient/pkg/analytics" 19 ) 20 21 func TestArgsClear(t *testing.T) { 22 f := newServerFixture(t) 23 24 createTiltfile(f, []string{"foo", "bar"}) 25 26 cmd := newArgsCmd(genericclioptions.NewTestIOStreamsDiscard()) 27 c := cmd.register() 28 err := c.Flags().Parse([]string{"--clear"}) 29 require.NoError(t, err) 30 err = cmd.run(f.ctx, c.Flags().Args()) 31 require.NoError(t, err) 32 33 require.Equal(t, 0, len(getTiltfile(f).Spec.Args)) 34 require.Equal(t, []analytics.CountEvent{ 35 {Name: "cmd.args", Tags: map[string]string{"clear": "true"}, N: 1}, 36 }, f.analytics.Counts) 37 } 38 39 func TestArgsNewValue(t *testing.T) { 40 f := newServerFixture(t) 41 42 createTiltfile(f, []string{"foo", "bar"}) 43 44 cmd := newArgsCmd(genericclioptions.NewTestIOStreamsDiscard()) 45 c := cmd.register() 46 err := c.Flags().Parse([]string{"--", "--foo", "bar"}) 47 require.NoError(t, err) 48 err = cmd.run(f.ctx, c.Flags().Args()) 49 require.NoError(t, err) 50 51 require.Equal(t, []string{"--foo", "bar"}, getTiltfile(f).Spec.Args) 52 require.Equal(t, []analytics.CountEvent{ 53 {Name: "cmd.args", Tags: map[string]string{"set": "true"}, N: 1}, 54 }, f.analytics.Counts) 55 } 56 57 func TestArgsClearAndNewValue(t *testing.T) { 58 f := newServerFixture(t) 59 60 createTiltfile(f, []string{"foo", "bar"}) 61 62 cmd := newArgsCmd(genericclioptions.NewTestIOStreamsDiscard()) 63 c := cmd.register() 64 err := c.Flags().Parse([]string{"--clear", "--", "--foo", "bar"}) 65 require.NoError(t, err) 66 err = cmd.run(f.ctx, c.Flags().Args()) 67 require.Error(t, err) 68 require.Contains(t, err.Error(), "--clear cannot be specified with other values") 69 } 70 71 func TestArgsNoChange(t *testing.T) { 72 f := newServerFixture(t) 73 74 createTiltfile(f, []string{"foo", "bar"}) 75 76 streams, _, _, errOut := genericclioptions.NewTestIOStreams() 77 cmd := newArgsCmd(streams) 78 c := cmd.register() 79 err := c.Flags().Parse([]string{"foo", "bar"}) 80 require.NoError(t, err) 81 err = cmd.run(f.ctx, c.Flags().Args()) 82 require.NoError(t, err) 83 require.Contains(t, errOut.String(), "no action taken") 84 } 85 86 func TestArgsEdit(t *testing.T) { 87 editorForString := func(contents string) string { 88 switch runtime.GOOS { 89 case "windows": 90 // This is trying to minimize windows weirdness: 91 // 1. If EDITOR includes a ` ` and a `\`, then the editor library will prepend a cmd /c, 92 // but then pass the whole $EDITOR as a single element of argv, while cmd /c 93 // seems to want everything as separate argvs. Since we're on Windows, any paths 94 // we get will have a `\`. 95 // 2. Windows' echo gave surprising quoting behavior that I didn't take the time to understand. 96 // So: generate one txt file that contains the desired contents and one bat file that 97 // simply writes the txt file to the first arg, so that the EDITOR we pass to the editor library 98 // has no spaces or quotes. 99 argFile, err := os.CreateTemp(t.TempDir(), "newargs*.txt") 100 require.NoError(t, err) 101 _, err = argFile.WriteString(contents) 102 require.NoError(t, err) 103 require.NoError(t, argFile.Close()) 104 f, err := os.CreateTemp(t.TempDir(), "writeargs*.bat") 105 require.NoError(t, err) 106 _, err = f.WriteString(fmt.Sprintf(`type %s > %%1`, argFile.Name())) 107 require.NoError(t, err) 108 err = f.Close() 109 require.NoError(t, err) 110 return f.Name() 111 default: 112 return fmt.Sprintf("echo %s >", shellescape.Quote(contents)) 113 } 114 } 115 116 for _, tc := range []struct { 117 name string 118 contents string 119 expectedArgs []string 120 expectedError string 121 }{ 122 {"simple", "baz quu", []string{"baz", "quu"}, ""}, 123 {"quotes", "baz 'quu quz'", []string{"baz", "quu quz"}, ""}, 124 {"comments ignored", " # test comment\n1 2\n # second test comment", []string{"1", "2"}, ""}, 125 {"parse error", "baz 'quu", nil, "Unterminated single-quoted string"}, 126 {"only comments", "# these are the tilt args", nil, "must have exactly one non-comment line, found zero. If you want to clear the args, use `tilt args --clear`"}, 127 {"multiple lines", "foo\nbar\n", nil, "cannot have multiple non-comment lines"}, 128 {"empty lines ignored", "1 2\n\n\n", []string{"1", "2"}, ""}, 129 {"dashes", "--foo --bar", []string{"--foo", "--bar"}, ""}, 130 {"quoted hash", "1 '2 # not a comment'", []string{"1", "2 # not a comment"}, ""}, 131 // TODO - fix comment parsing so the below passes 132 // {"mid-line comment", "1 2 # comment", []string{"1", "2"}, ""}, 133 } { 134 t.Run(tc.name, func(t *testing.T) { 135 f := newServerFixture(t) 136 137 contents := tc.contents 138 if runtime.GOOS == "windows" { 139 contents = strings.ReplaceAll(contents, "\n", "\r\n") 140 } 141 t.Setenv("EDITOR", editorForString(contents)) 142 143 originalArgs := []string{"foo", "bar"} 144 createTiltfile(f, originalArgs) 145 146 cmd := newArgsCmd(genericclioptions.NewTestIOStreamsDiscard()) 147 c := cmd.register() 148 err := c.Flags().Parse(nil) 149 require.NoError(t, err) 150 err = cmd.run(f.ctx, c.Flags().Args()) 151 if tc.expectedError != "" { 152 require.Error(t, err) 153 require.Contains(t, err.Error(), tc.expectedError) 154 } else { 155 require.NoError(t, err) 156 } 157 158 expectedArgs := originalArgs 159 if tc.expectedArgs != nil { 160 expectedArgs = tc.expectedArgs 161 } 162 require.Equal(t, expectedArgs, getTiltfile(f).Spec.Args) 163 var expectedCounts []analytics.CountEvent 164 if tc.expectedError == "" { 165 expectedCounts = []analytics.CountEvent{ 166 {Name: "cmd.args", Tags: map[string]string{"edit": "true"}, N: 1}, 167 } 168 } 169 require.Equal(t, expectedCounts, f.analytics.Counts) 170 }) 171 } 172 } 173 174 func createTiltfile(f *serverFixture, args []string) { 175 tf := v1alpha1.Tiltfile{ 176 ObjectMeta: metav1.ObjectMeta{ 177 Name: model.MainTiltfileManifestName.String(), 178 }, 179 Spec: v1alpha1.TiltfileSpec{Args: args}, 180 Status: v1alpha1.TiltfileStatus{}, 181 } 182 err := f.client.Create(f.ctx, &tf) 183 require.NoError(f.T(), err) 184 } 185 186 func getTiltfile(f *serverFixture) *v1alpha1.Tiltfile { 187 var tf v1alpha1.Tiltfile 188 err := f.client.Get(f.ctx, types.NamespacedName{Name: model.MainTiltfileManifestName.String()}, &tf) 189 require.NoError(f.T(), err) 190 return &tf 191 }