You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

300 lines
8.6 KiB

7 years ago
7 years ago
7 years ago
  1. package cli
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "io"
  7. "os"
  8. "path/filepath"
  9. "strconv"
  10. "strings"
  11. "testing"
  12. "github.com/spf13/cobra"
  13. "github.com/spf13/viper"
  14. "github.com/stretchr/testify/assert"
  15. "github.com/stretchr/testify/require"
  16. )
  17. func TestSetupEnv(t *testing.T) {
  18. ctx, cancel := context.WithCancel(context.Background())
  19. defer cancel()
  20. cases := []struct {
  21. args []string
  22. env map[string]string
  23. expected string
  24. }{
  25. {nil, nil, ""},
  26. {[]string{"--foobar", "bang!"}, nil, "bang!"},
  27. // make sure reset is good
  28. {nil, nil, ""},
  29. // test both variants of the prefix
  30. {nil, map[string]string{"DEMO_FOOBAR": "good"}, "good"},
  31. {nil, map[string]string{"DEMOFOOBAR": "silly"}, "silly"},
  32. // and that cli overrides env...
  33. {[]string{"--foobar", "important"},
  34. map[string]string{"DEMO_FOOBAR": "ignored"}, "important"},
  35. }
  36. for idx, tc := range cases {
  37. i := strconv.Itoa(idx)
  38. // test command that store value of foobar in local variable
  39. var foo string
  40. demo := &cobra.Command{
  41. Use: "demo",
  42. RunE: func(cmd *cobra.Command, args []string) error {
  43. foo = viper.GetString("foobar")
  44. return nil
  45. },
  46. }
  47. demo.Flags().String("foobar", "", "Some test value from config")
  48. cmd := PrepareBaseCmd(demo, "DEMO", "/qwerty/asdfgh") // some missing dir..
  49. viper.Reset()
  50. args := append([]string{cmd.Use}, tc.args...)
  51. err := RunWithArgs(ctx, cmd, args, tc.env)
  52. require.NoError(t, err, i)
  53. assert.Equal(t, tc.expected, foo, i)
  54. }
  55. }
  56. func tempDir(t *testing.T) string {
  57. t.Helper()
  58. cdir, err := os.MkdirTemp("", "test-cli")
  59. require.NoError(t, err)
  60. return cdir
  61. }
  62. // writeConfigVals writes a toml file with the given values.
  63. // It returns an error if writing was impossible.
  64. func writeConfigVals(dir string, vals map[string]string) error {
  65. lines := make([]string, 0, len(vals))
  66. for k, v := range vals {
  67. lines = append(lines, fmt.Sprintf("%s = %q", k, v))
  68. }
  69. data := strings.Join(lines, "\n")
  70. cfile := filepath.Join(dir, "config.toml")
  71. return os.WriteFile(cfile, []byte(data), 0600)
  72. }
  73. func TestSetupConfig(t *testing.T) {
  74. ctx, cancel := context.WithCancel(context.Background())
  75. defer cancel()
  76. // we pre-create two config files we can refer to in the rest of
  77. // the test cases.
  78. cval1 := "fubble"
  79. conf1 := tempDir(t)
  80. err := writeConfigVals(conf1, map[string]string{"boo": cval1})
  81. require.NoError(t, err)
  82. cases := []struct {
  83. args []string
  84. env map[string]string
  85. expected string
  86. expectedTwo string
  87. }{
  88. {nil, nil, "", ""},
  89. // setting on the command line
  90. {[]string{"--boo", "haha"}, nil, "haha", ""},
  91. {[]string{"--two-words", "rocks"}, nil, "", "rocks"},
  92. {[]string{"--home", conf1}, nil, cval1, ""},
  93. // test both variants of the prefix
  94. {nil, map[string]string{"RD_BOO": "bang"}, "bang", ""},
  95. {nil, map[string]string{"RD_TWO_WORDS": "fly"}, "", "fly"},
  96. {nil, map[string]string{"RDTWO_WORDS": "fly"}, "", "fly"},
  97. {nil, map[string]string{"RD_HOME": conf1}, cval1, ""},
  98. {nil, map[string]string{"RDHOME": conf1}, cval1, ""},
  99. }
  100. for idx, tc := range cases {
  101. i := strconv.Itoa(idx)
  102. // test command that store value of foobar in local variable
  103. var foo, two string
  104. boo := &cobra.Command{
  105. Use: "reader",
  106. RunE: func(cmd *cobra.Command, args []string) error {
  107. foo = viper.GetString("boo")
  108. two = viper.GetString("two-words")
  109. return nil
  110. },
  111. }
  112. boo.Flags().String("boo", "", "Some test value from config")
  113. boo.Flags().String("two-words", "", "Check out env handling -")
  114. cmd := PrepareBaseCmd(boo, "RD", "/qwerty/asdfgh") // some missing dir...
  115. viper.Reset()
  116. args := append([]string{cmd.Use}, tc.args...)
  117. err := RunWithArgs(ctx, cmd, args, tc.env)
  118. require.NoError(t, err, i)
  119. assert.Equal(t, tc.expected, foo, i)
  120. assert.Equal(t, tc.expectedTwo, two, i)
  121. }
  122. }
  123. type DemoConfig struct {
  124. Name string `mapstructure:"name"`
  125. Age int `mapstructure:"age"`
  126. Unused int `mapstructure:"unused"`
  127. }
  128. func TestSetupUnmarshal(t *testing.T) {
  129. ctx, cancel := context.WithCancel(context.Background())
  130. defer cancel()
  131. // we pre-create two config files we can refer to in the rest of
  132. // the test cases.
  133. cval1, cval2 := "someone", "else"
  134. conf1 := tempDir(t)
  135. err := writeConfigVals(conf1, map[string]string{"name": cval1})
  136. require.NoError(t, err)
  137. // even with some ignored fields, should be no problem
  138. conf2 := tempDir(t)
  139. err = writeConfigVals(conf2, map[string]string{"name": cval2, "foo": "bar"})
  140. require.NoError(t, err)
  141. // unused is not declared on a flag and remains from base
  142. base := DemoConfig{
  143. Name: "default",
  144. Age: 42,
  145. Unused: -7,
  146. }
  147. c := func(name string, age int) DemoConfig {
  148. r := base
  149. // anything set on the flags as a default is used over
  150. // the default config object
  151. r.Name = "from-flag"
  152. if name != "" {
  153. r.Name = name
  154. }
  155. if age != 0 {
  156. r.Age = age
  157. }
  158. return r
  159. }
  160. cases := []struct {
  161. args []string
  162. env map[string]string
  163. expected DemoConfig
  164. }{
  165. {nil, nil, c("", 0)},
  166. // setting on the command line
  167. {[]string{"--name", "haha"}, nil, c("haha", 0)},
  168. {[]string{"--home", conf1}, nil, c(cval1, 0)},
  169. // test both variants of the prefix
  170. {nil, map[string]string{"MR_AGE": "56"}, c("", 56)},
  171. {nil, map[string]string{"MR_HOME": conf1}, c(cval1, 0)},
  172. {[]string{"--age", "17"}, map[string]string{"MRHOME": conf2}, c(cval2, 17)},
  173. }
  174. for idx, tc := range cases {
  175. i := strconv.Itoa(idx)
  176. // test command that store value of foobar in local variable
  177. cfg := base
  178. marsh := &cobra.Command{
  179. Use: "marsh",
  180. RunE: func(cmd *cobra.Command, args []string) error {
  181. return viper.Unmarshal(&cfg)
  182. },
  183. }
  184. marsh.Flags().String("name", "from-flag", "Some test value from config")
  185. // if we want a flag to use the proper default, then copy it
  186. // from the default config here
  187. marsh.Flags().Int("age", base.Age, "Some test value from config")
  188. cmd := PrepareBaseCmd(marsh, "MR", "/qwerty/asdfgh") // some missing dir...
  189. viper.Reset()
  190. args := append([]string{cmd.Use}, tc.args...)
  191. err := RunWithArgs(ctx, cmd, args, tc.env)
  192. require.NoError(t, err, i)
  193. assert.Equal(t, tc.expected, cfg, i)
  194. }
  195. }
  196. func TestSetupTrace(t *testing.T) {
  197. ctx, cancel := context.WithCancel(context.Background())
  198. defer cancel()
  199. cases := []struct {
  200. args []string
  201. env map[string]string
  202. long bool
  203. expected string
  204. }{
  205. {nil, nil, false, "trace flag = false"},
  206. {[]string{"--trace"}, nil, true, "trace flag = true"},
  207. {[]string{"--no-such-flag"}, nil, false, "unknown flag: --no-such-flag"},
  208. {nil, map[string]string{"DBG_TRACE": "true"}, true, "trace flag = true"},
  209. }
  210. for idx, tc := range cases {
  211. i := strconv.Itoa(idx)
  212. // test command that store value of foobar in local variable
  213. trace := &cobra.Command{
  214. Use: "trace",
  215. RunE: func(cmd *cobra.Command, args []string) error {
  216. return fmt.Errorf("trace flag = %t", viper.GetBool(TraceFlag))
  217. },
  218. }
  219. cmd := PrepareBaseCmd(trace, "DBG", "/qwerty/asdfgh") // some missing dir..
  220. viper.Reset()
  221. args := append([]string{cmd.Use}, tc.args...)
  222. stdout, stderr, err := runCaptureWithArgs(ctx, cmd, args, tc.env)
  223. require.Error(t, err, i)
  224. require.Equal(t, "", stdout, i)
  225. require.NotEqual(t, "", stderr, i)
  226. msg := strings.Split(stderr, "\n")
  227. desired := fmt.Sprintf("ERROR: %s", tc.expected)
  228. assert.Equal(t, desired, msg[0], i, msg)
  229. if tc.long && assert.True(t, len(msg) > 2, i) {
  230. // the next line starts the stack trace...
  231. assert.Contains(t, stderr, "TestSetupTrace", i)
  232. assert.Contains(t, stderr, "setup_test.go", i)
  233. }
  234. }
  235. }
  236. // runCaptureWithArgs executes the given command with the specified command
  237. // line args and environmental variables set. It returns string fields
  238. // representing output written to stdout and stderr, additionally any error
  239. // from cmd.Execute() is also returned
  240. func runCaptureWithArgs(ctx context.Context, cmd *cobra.Command, args []string, env map[string]string) (stdout, stderr string, err error) {
  241. oldout, olderr := os.Stdout, os.Stderr // keep backup of the real stdout
  242. rOut, wOut, _ := os.Pipe()
  243. rErr, wErr, _ := os.Pipe()
  244. os.Stdout, os.Stderr = wOut, wErr
  245. defer func() {
  246. os.Stdout, os.Stderr = oldout, olderr // restoring the real stdout
  247. }()
  248. // copy the output in a separate goroutine so printing can't block indefinitely
  249. copyStd := func(reader *os.File) *(chan string) {
  250. stdC := make(chan string)
  251. go func() {
  252. var buf bytes.Buffer
  253. // io.Copy will end when we call reader.Close() below
  254. io.Copy(&buf, reader) //nolint:errcheck //ignore error
  255. select {
  256. case <-cmd.Context().Done():
  257. case stdC <- buf.String():
  258. }
  259. }()
  260. return &stdC
  261. }
  262. outC := copyStd(rOut)
  263. errC := copyStd(rErr)
  264. // now run the command
  265. err = RunWithArgs(ctx, cmd, args, env)
  266. // and grab the stdout to return
  267. wOut.Close()
  268. wErr.Close()
  269. stdout = <-*outC
  270. stderr = <-*errC
  271. return stdout, stderr, err
  272. }