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.

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