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  }