...

Source file src/edge-infra.dev/pkg/lib/cli/sink/command_test.go

Documentation: edge-infra.dev/pkg/lib/cli/sink

     1  package sink
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/peterbourgon/ff/v3"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"edge-infra.dev/pkg/lib/cli/rags"
    14  )
    15  
    16  type testExtension struct {
    17  	name              string
    18  	finishedBeforeRun bool
    19  	finishedAfterRun  bool
    20  	stringFlag        string
    21  	boolFlag          bool
    22  }
    23  
    24  func (ext *testExtension) RegisterFlags(rs *rags.RagSet) {
    25  	if ext.name == "" {
    26  		ext.name = "test-extension"
    27  	}
    28  	rs.StringVar(&ext.stringFlag, ext.name+"-string-flag", "default string value", "string usage message")
    29  	rs.BoolVar(&ext.boolFlag, ext.name+"-bool-flag", false, "bool usage message")
    30  }
    31  
    32  type testCtxKey = struct{}
    33  
    34  func (ext *testExtension) BeforeRun(ctx context.Context, r Run) (context.Context, Run, error) {
    35  	ext.finishedBeforeRun = true
    36  	return context.WithValue(ctx, &testCtxKey{}, "value"), r, nil
    37  }
    38  
    39  func (ext *testExtension) AfterRun(ctx context.Context, r Run) (context.Context, Run, error) {
    40  	ext.finishedAfterRun = true
    41  	return ctx, r, nil
    42  }
    43  
    44  type faultyExtension struct{}
    45  
    46  func (ext *faultyExtension) RegisterFlags(_ *rags.RagSet) {}
    47  
    48  func (ext *faultyExtension) BeforeRun(ctx context.Context, r Run) (context.Context, Run, error) {
    49  	return ctx, r, fmt.Errorf("Faulty extension BeforeRun")
    50  }
    51  
    52  func (ext *faultyExtension) AfterRun(ctx context.Context, r Run) (context.Context, Run, error) {
    53  	return ctx, r, fmt.Errorf("Faulty extension AfterRun")
    54  }
    55  
    56  func TestNames(t *testing.T) {
    57  	cmd := newTestCmd()
    58  
    59  	// Force command tree to be computed.
    60  	require.NoError(t, cmd.compute())
    61  
    62  	assert.Equal(t, cmd.Use, cmd.Name())
    63  	assert.Equal(t, cmd.Use, cmd.LongName())
    64  	for _, scmd := range cmd.Commands {
    65  		assert.Equal(t, scmd.Use, scmd.Name())
    66  		assert.Equal(t, scmd.parent.LongName()+" "+scmd.Use, scmd.LongName())
    67  	}
    68  
    69  	t.Run("Name", func(t *testing.T) {
    70  		tcs := map[string]struct {
    71  			use string
    72  			exp string
    73  		}{
    74  			"simple":           {"root", "root"},
    75  			"options and args": {"foo [flags] <argument> <argument>", "foo"},
    76  			"trailing space":   {"foo  ", "foo"},
    77  		}
    78  
    79  		for name, tc := range tcs {
    80  			t.Run(name, func(t *testing.T) {
    81  				c := &Command{Use: tc.use}
    82  				assert.Equal(t, tc.exp, c.Name())
    83  			})
    84  		}
    85  	})
    86  }
    87  
    88  func TestUseLine(t *testing.T) {
    89  	var (
    90  		root = &Command{Use: "root [flags]"}
    91  		foo  = &Command{Use: "foo [command]"}
    92  		get  = &Command{Use: "get [flags] <object>"}
    93  		del  = &Command{Use: "delete [flags] <object>"}
    94  	)
    95  	root.Commands = []*Command{foo}
    96  	foo.Commands = []*Command{get, del}
    97  
    98  	// Force command tree to be computed.
    99  	require.NoError(t, root.compute())
   100  
   101  	tcs := []struct {
   102  		cmd *Command
   103  		exp string
   104  	}{
   105  		{root, root.Use},
   106  		{foo, "root " + foo.Use},
   107  		{get, "root foo " + get.Use},
   108  		{del, "root foo " + del.Use},
   109  	}
   110  
   111  	for _, tc := range tcs {
   112  		assert.Equal(t, tc.exp, useline(tc.cmd))
   113  	}
   114  }
   115  
   116  func TestExtensions(t *testing.T) {
   117  	// TODO: should test context chaining (multiple ext modify context, verify
   118  	// all changes are present)
   119  	t.Run("extension errors", func(t *testing.T) {
   120  		cmd := &Command{Use: "cli", Extensions: []Extension{&faultyExtension{}}}
   121  		ctx := context.Background()
   122  		r := newRun(cmd)
   123  		var err error
   124  		ctx, r, err = cmd.beforeRun(ctx, r)
   125  		assert.Error(t, err)
   126  		_, _, err = cmd.afterRun(ctx, r)
   127  		assert.Error(t, err)
   128  	})
   129  	t.Run("working extension", func(t *testing.T) {
   130  		ext := &testExtension{}
   131  		cmd := &Command{Use: "cli", Extensions: []Extension{ext}}
   132  		ctx := context.Background()
   133  		r := newRun(cmd)
   134  		var err error
   135  		ctx, r, err = cmd.beforeRun(ctx, r)
   136  		assert.NoError(t, err)
   137  		assert.True(t, ext.finishedBeforeRun)
   138  		assert.Equal(t, "value", ctx.Value(&testCtxKey{}))
   139  		ctx, _, err = cmd.afterRun(ctx, r)
   140  		assert.NoError(t, err)
   141  		assert.True(t, ext.finishedAfterRun)
   142  		assert.Equal(t, "value", ctx.Value(&testCtxKey{}))
   143  	})
   144  }
   145  
   146  func TestDefaultUsageFunc(t *testing.T) {
   147  	tcs := map[string]*Command{
   148  		"short usage": {
   149  			Use:   "foo",
   150  			Short: "Foo the bar",
   151  		},
   152  		"long usage": {
   153  			Use:  "foo",
   154  			Long: "This is a paragraph about Fooing The Bar",
   155  		},
   156  		"long usage overrides short": {
   157  			Use:   "foo",
   158  			Short: "Foo the bar",
   159  			Long:  "This is a paragraph about Fooing The Bar",
   160  		},
   161  		"subcommands": {
   162  			Use:      "foo",
   163  			Short:    "Foo the bar",
   164  			Long:     "This is a paragraph about Fooing The Bar",
   165  			Commands: []*Command{{Use: "bar"}},
   166  		},
   167  		"flags": {
   168  			Use:      "foo",
   169  			Short:    "Foo the bar",
   170  			Long:     "This is a paragraph about Fooing The Bar",
   171  			Commands: []*Command{{Use: "bar"}},
   172  			Flags: []*rags.Rag{
   173  				{Name: "you-see-me", Value: &rags.Bool{}},
   174  			},
   175  		},
   176  		"no help text": {
   177  			Use: "foo",
   178  		},
   179  	}
   180  
   181  	for name, c := range tcs {
   182  		name := name
   183  		c := c
   184  		t.Run(name, func(t *testing.T) {
   185  			t.Parallel()
   186  
   187  			// Force command to be computed
   188  			require.NoError(t, c.compute())
   189  
   190  			usage := defaultUsageFn(c)
   191  			lines := strings.Split(usage, "\n")
   192  
   193  			t.Log("usage", "\n"+usage)
   194  			for i, l := range lines {
   195  				t.Log(i, l)
   196  			}
   197  
   198  			switch {
   199  			case c.Long != "":
   200  				assert.Equal(t, c.Long, lines[0])
   201  			case c.Short != "":
   202  				assert.Equal(t, c.Short, lines[0])
   203  			default:
   204  				assert.Equal(t, "Usage:", lines[0])
   205  			}
   206  
   207  			if len(c.Commands) == 0 {
   208  				assert.False(t, strings.Contains(usage, "\nCommands:"))
   209  			} else {
   210  				assert.True(t, strings.Contains(usage, "\nCommands:"))
   211  			}
   212  
   213  			if len(c.Flags) == 0 {
   214  				assert.False(t, strings.Contains(usage, "\nFlags:"))
   215  			} else {
   216  				assert.True(t, strings.Contains(usage, "\nFlags:"))
   217  			}
   218  
   219  			assert.True(t, lines[len(lines)-1] == "")
   220  			assert.True(t, lines[len(lines)-2] != "")
   221  		})
   222  	}
   223  }
   224  
   225  func TestParse(t *testing.T) {
   226  	t.Parallel()
   227  
   228  	tcs := map[string]struct {
   229  		cmdIdx []int
   230  		args   []string
   231  	}{
   232  		"root":                {[]int{}, []string{}},
   233  		"root foo":            {[]int{0}, []string{"foo"}},
   234  		"root foo view":       {[]int{0, 0}, []string{"foo", "view"}},
   235  		"root foo delete":     {[]int{0, 1}, []string{"foo", "delete"}},
   236  		"root bar":            {[]int{1}, []string{"bar"}},
   237  		"root bar baz":        {[]int{1, 4}, []string{"bar", "baz"}},
   238  		"root bar baz delete": {[]int{1, 4, 1}, []string{"bar", "baz", "delete"}},
   239  	}
   240  
   241  	for name, tc := range tcs {
   242  		name := name
   243  		tc := tc
   244  		t.Run(name, func(t *testing.T) {
   245  			t.Parallel()
   246  
   247  			cmd := newTestCmd()
   248  			assert.NoError(t, cmd.Parse(tc.args))
   249  			assert.ElementsMatch(t, cmd.args, tc.args)
   250  			t.Log("args", cmd.args)
   251  
   252  			expCmd := cmd
   253  			cmds := expCmd.Commands
   254  			for _, idx := range tc.cmdIdx {
   255  				for i, s := range cmds {
   256  					if i == idx {
   257  						assert.NotNil(t, s.selected)
   258  					} else {
   259  						assert.Nil(t, s.selected)
   260  					}
   261  				}
   262  				expCmd = cmds[idx]
   263  				cmds = expCmd.Commands
   264  			}
   265  		})
   266  	}
   267  }
   268  
   269  func TestCommand_compute(t *testing.T) {
   270  	t.Parallel()
   271  	cmd := newTestCmd()
   272  
   273  	// Force command tree to be computed.
   274  	require.NoError(t, cmd.compute())
   275  
   276  	testComputedCmd(t, cmd)
   277  
   278  	t.Run("parent as child", func(t *testing.T) {
   279  		t.Parallel()
   280  		cmd := newTestCmd()
   281  		cmd.Commands = append(cmd.Commands, cmd)
   282  		assert.Error(t, cmd.compute())
   283  	})
   284  
   285  	t.Run("duplicate children", func(t *testing.T) {
   286  		t.Parallel()
   287  		cmd := newTestCmd()
   288  		cmd.Commands = append(cmd.Commands, cmd.Commands[0])
   289  		assert.Error(t, cmd.compute())
   290  	})
   291  }
   292  
   293  func testComputedCmd(t *testing.T, cmd *Command) {
   294  	t.Helper()
   295  	a := assert.New(t)
   296  
   297  	if cmd.HasParent() {
   298  		p := cmd.Parent()
   299  
   300  		switch {
   301  		case len(p.AllParsingOptions()) > 0:
   302  			exp := append(p.AllParsingOptions(), cmd.Options...)
   303  			actual := cmd.AllParsingOptions()
   304  			a.Len(actual, len(exp))
   305  		default:
   306  			a.Len(cmd.AllParsingOptions(), len(cmd.Options))
   307  		}
   308  
   309  		a.Equal(p.LongName()+" "+cmd.Name(), cmd.LongName())
   310  	} else {
   311  		a.Equal(cmd.Name(), cmd.LongName())
   312  		a.ElementsMatch(cmd.Options, cmd.AllParsingOptions())
   313  	}
   314  
   315  	a.NotNil(cmd.Exec)
   316  
   317  	for _, scmd := range cmd.Commands {
   318  		a.Same(cmd, scmd.parent)
   319  		testComputedCmd(t, scmd)
   320  	}
   321  }
   322  
   323  // add some extensions and options throughout
   324  func newTestCmd() *Command {
   325  	return &Command{
   326  		Use: "root",
   327  		Commands: []*Command{
   328  			{
   329  				Use:        "foo",
   330  				Extensions: []Extension{&testExtension{}},
   331  				Commands: []*Command{
   332  					{Use: "view"},
   333  					{Use: "delete"},
   334  					{Use: "list"},
   335  					{Use: "create"},
   336  				},
   337  			},
   338  			{
   339  				Use:     "bar",
   340  				Options: []ff.Option{ff.WithEnvVarNoPrefix()},
   341  				Commands: []*Command{
   342  					{Use: "view"},
   343  					{Use: "delete"},
   344  					{Use: "list"},
   345  					{Use: "create"},
   346  					{Use: "baz",
   347  						Commands: []*Command{
   348  							{Use: "view", Options: []ff.Option{ff.WithIgnoreUndefined(true)}},
   349  							{Use: "delete"},
   350  							{Use: "list"},
   351  							{Use: "create"},
   352  						},
   353  					},
   354  				},
   355  			},
   356  		},
   357  	}
   358  }
   359  

View as plain text