1
0

registry_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. package commands
  2. import (
  3. "bytes"
  4. "errors"
  5. "flag"
  6. "strings"
  7. "testing"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. )
  11. func testRegistry() *Registry {
  12. r := New()
  13. r.Register(&Command{
  14. Name: "served",
  15. ShortDescription: "a command this surface serves",
  16. Callback: func(fs any, a []string, w StringWriter) error {
  17. return w.WriteLine("read " + strings.Join(a, ","))
  18. },
  19. })
  20. r.Register(&Command{
  21. Name: "withheld",
  22. ShortDescription: "a command this surface withholds",
  23. Callback: func(fs any, a []string, w StringWriter) error {
  24. return w.WriteLine("wrote")
  25. },
  26. })
  27. return r
  28. }
  29. // TestFlagUsageGoesToTheWriter pins that a parse failure's usage lands on the command's
  30. // writer.
  31. func TestFlagUsageGoesToTheWriter(t *testing.T) {
  32. r := New()
  33. r.Register(&Command{
  34. Name: "emit",
  35. ShortDescription: "emits",
  36. Flags: func() (*flag.FlagSet, any) {
  37. fl := flag.NewFlagSet("", flag.ContinueOnError)
  38. fl.Bool("json", false, "emit json")
  39. return fl, nil
  40. },
  41. Callback: func(fs any, a []string, w StringWriter) error { return nil },
  42. })
  43. merged := &bytes.Buffer{}
  44. require.Error(t, r.DispatchArgs([]string{"emit", "-nope"}, NewStringWriter(merged)))
  45. assert.Contains(t, merged.String(), "flag provided but not defined")
  46. }
  47. // TestDispatchReportsUnparseableLine: a line shlex rejects, an unbalanced quote, is a
  48. // failure the user is told about.
  49. func TestDispatchReportsUnparseableLine(t *testing.T) {
  50. out := &bytes.Buffer{}
  51. err := testRegistry().Dispatch(`served "unterminated`, NewStringWriter(out))
  52. var f *Failure
  53. require.ErrorAs(t, err, &f)
  54. assert.Equal(t, f.Msg+"\n", out.String())
  55. assert.Contains(t, out.String(), "could not parse the command line: ")
  56. }
  57. // TestFailTellsTheUserAndTheSurface: Fail writes the message and returns it as a Failure;
  58. // when the write fails, the user heard nothing, so the write error is what comes back.
  59. func TestFailTellsTheUserAndTheSurface(t *testing.T) {
  60. out := &bytes.Buffer{}
  61. err := Fail(NewStringWriter(out), "no such tunnel")
  62. var f *Failure
  63. require.ErrorAs(t, err, &f)
  64. assert.Equal(t, "no such tunnel", f.Msg)
  65. assert.Equal(t, "no such tunnel\n", out.String())
  66. err = Fail(NewStringWriter(failingWriter{}), "no such tunnel")
  67. require.EqualError(t, err, "gone")
  68. assert.NotErrorAs(t, err, &f)
  69. }
  70. type failingWriter struct{}
  71. func (failingWriter) Write([]byte) (int, error) { return 0, errors.New("gone") }
  72. // TestAllSortsByName: registration order does not matter.
  73. func TestAllSortsByName(t *testing.T) {
  74. r := New()
  75. for _, n := range []Name{"zz", Version, "aa", ChangeRemote} {
  76. r.Register(&Command{Name: n})
  77. }
  78. var got []Name
  79. for _, c := range r.All() {
  80. got = append(got, c.Name)
  81. }
  82. assert.Equal(t, []Name{"aa", ChangeRemote, Help, Version, "zz"}, got)
  83. }
  84. // TestHelpFlagAgreesWithHelpCommand pins that -h answers through the registered help
  85. // command, and that help on a clone describes the clone: a command registered only there
  86. // is known to both -h and `help <name>`, and one withheld from it is known to neither.
  87. func TestHelpFlagAgreesWithHelpCommand(t *testing.T) {
  88. r := testRegistry()
  89. c := r.Clone()
  90. c.Register(&Command{
  91. Name: Logout,
  92. ShortDescription: "ends the session",
  93. Callback: func(fs any, a []string, w StringWriter) error { return nil },
  94. })
  95. flag := &bytes.Buffer{}
  96. c.Dispatch("logout -h", NewStringWriter(flag))
  97. cmd := &bytes.Buffer{}
  98. c.Dispatch("help logout", NewStringWriter(cmd))
  99. assert.Equal(t, cmd.String(), flag.String())
  100. assert.Equal(t, "logout - ends the session\n", flag.String())
  101. // The source never learns of it.
  102. src := &bytes.Buffer{}
  103. r.Dispatch("help logout", NewStringWriter(src))
  104. assert.Equal(t, "Command not available logout\n", src.String())
  105. // And a command both hold is described either way.
  106. flag.Reset()
  107. c.Dispatch("served -h", NewStringWriter(flag))
  108. assert.Equal(t, "served - a command this surface serves\n", flag.String())
  109. }
  110. // TestCloneHelpListsTheClone: the command list help prints is the clone's, so a clone
  111. // that withholds a command does not name it, and the source still does.
  112. func TestCloneHelpListsTheClone(t *testing.T) {
  113. r := testRegistry()
  114. c := New()
  115. c.Register(r.Get("served"))
  116. c = c.Clone()
  117. out := &bytes.Buffer{}
  118. c.Dispatch("help", NewStringWriter(out))
  119. assert.Equal(t, "Available commands:\n"+
  120. "help - prints available commands or help <command> for specific usage info\n"+
  121. "served - a command this surface serves\n\n", out.String())
  122. out.Reset()
  123. c.Dispatch("help withheld", NewStringWriter(out))
  124. assert.Equal(t, "Command not available withheld\n", out.String())
  125. out.Reset()
  126. r.Dispatch("help withheld", NewStringWriter(out))
  127. assert.Equal(t, "withheld - a command this surface withholds\n", out.String())
  128. }
  129. // TestDispatchReturnsTheCommandsError: what the callback returns reaches the caller, so
  130. // a surface can log it.
  131. func TestDispatchReturnsTheCommandsError(t *testing.T) {
  132. r := New()
  133. r.Register(&Command{
  134. Name: "fails",
  135. Callback: func(fs any, a []string, w StringWriter) error {
  136. return errors.New("boom")
  137. },
  138. })
  139. out := &bytes.Buffer{}
  140. require.EqualError(t, r.Dispatch("fails", NewStringWriter(out)), "boom")
  141. assert.Empty(t, out.String(), "a command's own error is not shown; the command speaks for itself")
  142. }
  143. // TestUnknownCommandNamesTheWord: only the first word is looked up, so only it is
  144. // reported, followed by the command list, and the line is a failure to the surface.
  145. func TestUnknownCommandNamesTheWord(t *testing.T) {
  146. out := &bytes.Buffer{}
  147. err := testRegistry().Dispatch(`nope "with args"`, NewStringWriter(out))
  148. var f *Failure
  149. require.ErrorAs(t, err, &f)
  150. assert.Equal(t, "did not understand: nope", f.Msg)
  151. assert.True(t, strings.HasPrefix(out.String(), "did not understand: nope\nAvailable commands:\n"), out.String())
  152. }