zone_selection_test.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. package main
  2. import (
  3. "bytes"
  4. "reflect"
  5. "strings"
  6. "testing"
  7. "github.com/spf13/cobra"
  8. "netdome.biz/paul/qmk-rgb/internal/via"
  9. )
  10. // A command that writes lighting and named no zone used to write every channel
  11. // the keyboard has, which is the widest thing it can do and not something anybody
  12. // asked for. Its argument count refuses it now, before the keyboard is opened, so
  13. // a script gets the same answer whichever of them it called.
  14. func TestLightingCommandsRefuseToWriteWithoutAZone(t *testing.T) {
  15. cases := []struct {
  16. name string
  17. cmd func() *cobra.Command
  18. args []string
  19. }{
  20. {"effect", NewEffectCmd, nil},
  21. {"brightness", NewBrightnessCmd, []string{"160"}},
  22. {"speed", NewSpeedCmd, []string{"2"}},
  23. {"color", NewColorCmd, []string{"00ff00"}},
  24. {"enable", NewEnableCmd, nil},
  25. {"disable", NewDisableCmd, nil},
  26. }
  27. for _, tc := range cases {
  28. t.Run(tc.name, func(t *testing.T) {
  29. proto := &fakeZoneProtocol{failAt: -1}
  30. t.Cleanup(impact80Target(t, proto))
  31. var out bytes.Buffer
  32. cmd := tc.cmd()
  33. cmd.SetOut(&out)
  34. cmd.SetErr(&out)
  35. cmd.SilenceErrors = true
  36. cmd.SilenceUsage = true
  37. cmd.SetArgs(tc.args)
  38. err := cmd.Execute()
  39. if err == nil {
  40. t.Fatal("Execute() expected the missing zone to be refused, got nil")
  41. }
  42. // The message has to say which argument was missing, or the user
  43. // cannot act on it without reading the source.
  44. if !strings.Contains(err.Error(), "zone") {
  45. t.Errorf("error = %q, want it to name the missing zone", err)
  46. }
  47. if !strings.Contains(err.Error(), "usage:") {
  48. t.Errorf("error = %q, want the usage line so the order of the arguments is visible", err)
  49. }
  50. if len(proto.reports) != 0 {
  51. t.Errorf("reports = %v, want no write before the zone was named", proto.reports)
  52. }
  53. if out.Len() != 0 {
  54. t.Errorf("output = %q, want nothing written before the zone was named", out.String())
  55. }
  56. })
  57. }
  58. }
  59. // A list of zones reaches exactly the channels it names, and no others.
  60. func TestZoneListWritesEveryNamedChannel(t *testing.T) {
  61. cases := []struct {
  62. zone string
  63. want []commandReport
  64. }{
  65. {"logo", []commandReport{{channel: 2, param: 1, value: 160}}},
  66. {"side,logo", []commandReport{
  67. {channel: 2, param: 1, value: 160},
  68. {channel: 4, param: 1, value: 160},
  69. }},
  70. // The order written is not the order applied: channels come back in
  71. // channel order, so a script sees one order for every spelling.
  72. {"logo, side", []commandReport{
  73. {channel: 2, param: 1, value: 160},
  74. {channel: 4, param: 1, value: 160},
  75. }},
  76. // One name twice is one channel, not a double write.
  77. {"logo,logo", []commandReport{{channel: 2, param: 1, value: 160}}},
  78. // A display name and the subsystem name of the same channel are one
  79. // channel, and so is a name that matches through both.
  80. {"rgblight", []commandReport{{channel: 2, param: 1, value: 160}}},
  81. {"Backlight", []commandReport{{channel: 3, param: 1, value: 160}}},
  82. {"backlight", []commandReport{{channel: 3, param: 1, value: 160}}},
  83. }
  84. for _, tc := range cases {
  85. t.Run(tc.zone, func(t *testing.T) {
  86. proto := &fakeZoneProtocol{failAt: -1}
  87. t.Cleanup(impact80Target(t, proto))
  88. var out bytes.Buffer
  89. cmd := NewBrightnessCmd()
  90. cmd.SetOut(&out)
  91. cmd.SetErr(&out)
  92. cmd.SetArgs([]string{tc.zone, "160"})
  93. if err := cmd.Execute(); err != nil {
  94. t.Fatalf("brightness %s 160 returned error: %v", tc.zone, err)
  95. }
  96. if !reflect.DeepEqual(proto.reports, tc.want) {
  97. t.Errorf("reports = %v, want %v", proto.reports, tc.want)
  98. }
  99. })
  100. }
  101. }
  102. // all is every channel, and naming it next to one more channel is the same
  103. // request: the union is the whole keyboard either way.
  104. func TestAllMeansEveryChannel(t *testing.T) {
  105. cases := []string{"all", "ALL", "side,all", "all,logo", " backlight , all "}
  106. for _, zone := range cases {
  107. t.Run(zone, func(t *testing.T) {
  108. proto := &fakeZoneProtocol{failAt: -1}
  109. t.Cleanup(impact80Target(t, proto))
  110. var out bytes.Buffer
  111. cmd := NewBrightnessCmd()
  112. cmd.SetOut(&out)
  113. cmd.SetErr(&out)
  114. cmd.SetArgs([]string{zone, "160"})
  115. if err := cmd.Execute(); err != nil {
  116. t.Fatalf("brightness %q 160 returned error: %v", zone, err)
  117. }
  118. want := []commandReport{
  119. {channel: 2, param: 1, value: 160},
  120. {channel: 3, param: 1, value: 160},
  121. {channel: 4, param: 1, value: 160},
  122. }
  123. if !reflect.DeepEqual(proto.reports, want) {
  124. t.Errorf("reports = %v, want %v", proto.reports, want)
  125. }
  126. })
  127. }
  128. }
  129. // A zone the keyboard does not have is refused, and where a list names several,
  130. // every one that is missing is named: writing two of the three channels asked for
  131. // and reporting a success is the failure this prevents.
  132. func TestZoneListRefusesAChannelTheKeyboardLacks(t *testing.T) {
  133. cases := []struct {
  134. zone string
  135. want []string
  136. }{
  137. {"led_matrix", []string{"led_matrix"}},
  138. {"logo,led_matrix", []string{"led_matrix"}},
  139. {"logo,led_matrix,backlight", []string{"led_matrix"}},
  140. }
  141. for _, tc := range cases {
  142. t.Run(tc.zone, func(t *testing.T) {
  143. proto := &fakeZoneProtocol{failAt: -1}
  144. t.Cleanup(impact80Target(t, proto))
  145. var out bytes.Buffer
  146. cmd := NewBrightnessCmd()
  147. cmd.SetOut(&out)
  148. cmd.SetErr(&out)
  149. cmd.SetArgs([]string{tc.zone, "160"})
  150. err := cmd.Execute()
  151. if err == nil {
  152. t.Fatalf("brightness %s 160 = nil error, want the missing channel refused", tc.zone)
  153. }
  154. for _, want := range tc.want {
  155. if !strings.Contains(err.Error(), want) {
  156. t.Errorf("error = %q, want it to name %q", err, want)
  157. }
  158. }
  159. if len(proto.reports) != 0 {
  160. t.Errorf("reports = %v, want nothing written when a named channel is absent", proto.reports)
  161. }
  162. })
  163. }
  164. }
  165. // A name nobody wrote has to say what the alternatives are, and a list that names
  166. // one of them has to say which of the list it was.
  167. func TestUnknownZoneNamesTheVocabulary(t *testing.T) {
  168. cases := []struct {
  169. zone string
  170. want []string
  171. }{
  172. {"nonsense", []string{"nonsense", "rgb_matrix", "all"}},
  173. {"logo,nonsense", []string{"nonsense", `in "logo,nonsense"`}},
  174. }
  175. for _, tc := range cases {
  176. t.Run(tc.zone, func(t *testing.T) {
  177. proto := &fakeZoneProtocol{failAt: -1}
  178. t.Cleanup(impact80Target(t, proto))
  179. var out bytes.Buffer
  180. cmd := NewBrightnessCmd()
  181. cmd.SetOut(&out)
  182. cmd.SetErr(&out)
  183. cmd.SetArgs([]string{tc.zone, "160"})
  184. err := cmd.Execute()
  185. if err == nil {
  186. t.Fatalf("brightness %s 160 = nil error, want the unknown zone refused", tc.zone)
  187. }
  188. for _, want := range tc.want {
  189. if !strings.Contains(err.Error(), want) {
  190. t.Errorf("error = %q, want it to contain %q", err, want)
  191. }
  192. }
  193. })
  194. }
  195. }
  196. // A trailing or doubled comma names no channel, and guessing which one was meant
  197. // is worse than saying so.
  198. func TestZoneListRefusesAnEmptyName(t *testing.T) {
  199. for _, zone := range []string{"logo,", ",logo", "logo,,side", "logo, "} {
  200. t.Run(zone, func(t *testing.T) {
  201. proto := &fakeZoneProtocol{failAt: -1}
  202. t.Cleanup(impact80Target(t, proto))
  203. var out bytes.Buffer
  204. cmd := NewBrightnessCmd()
  205. cmd.SetOut(&out)
  206. cmd.SetErr(&out)
  207. cmd.SetArgs([]string{zone, "160"})
  208. err := cmd.Execute()
  209. if err == nil {
  210. t.Fatalf("brightness %q 160 = nil error, want the empty name refused", zone)
  211. }
  212. if !strings.Contains(err.Error(), "empty") {
  213. t.Errorf("error = %q, want it to say the list has an empty name", err)
  214. }
  215. if len(proto.reports) != 0 {
  216. t.Errorf("reports = %v, want no write", proto.reports)
  217. }
  218. })
  219. }
  220. }
  221. // A zone that a definition file gives to two channels of the same board would be
  222. // a silent retarget, so the list resolves it to neither.
  223. func TestZoneListRefusesANameTwoChannelsShare(t *testing.T) {
  224. proto := &fakeZoneProtocol{failAt: -1}
  225. display := map[uint16]string{2: "logo", 3: "logo", 4: "side"}
  226. t.Cleanup(stubOpenTarget(t, proto, display, impact80Channels(), 0x36B0, 0x309F))
  227. var out bytes.Buffer
  228. cmd := NewBrightnessCmd()
  229. cmd.SetOut(&out)
  230. cmd.SetErr(&out)
  231. cmd.SetArgs([]string{"logo", "160"})
  232. err := cmd.Execute()
  233. if err == nil {
  234. t.Fatal("Execute() = nil error, want the ambiguous name refused")
  235. }
  236. if !strings.Contains(err.Error(), "several channels") {
  237. t.Errorf("error = %q, want it to say the name is ambiguous", err)
  238. }
  239. if len(proto.reports) != 0 {
  240. t.Errorf("reports = %v, want no write", proto.reports)
  241. }
  242. }
  243. // The value is parsed before the keyboard is opened, so a typo in it costs
  244. // nothing and reports where the mistake is.
  245. func TestReversedArgumentsFailOnTheValue(t *testing.T) {
  246. proto := &fakeZoneProtocol{failAt: -1}
  247. opened := false
  248. original := openTarget
  249. t.Cleanup(func() { openTarget = original })
  250. stub := openTarget
  251. openTarget = func(zone string) (rgbProtocol, targetDeviceData, []via.Channel, error) {
  252. opened = true
  253. return stub(zone)
  254. }
  255. var out bytes.Buffer
  256. cmd := NewBrightnessCmd()
  257. cmd.SetOut(&out)
  258. cmd.SetErr(&out)
  259. cmd.SetArgs([]string{"160", "logo"})
  260. err := cmd.Execute()
  261. if err == nil {
  262. t.Fatal("Execute() = nil error, want the reversed arguments refused")
  263. }
  264. if !strings.Contains(err.Error(), "invalid value") {
  265. t.Errorf("error = %q, want it to say the value could not be read", err)
  266. }
  267. if opened {
  268. t.Error("the keyboard was opened, want the value rejected first")
  269. }
  270. if len(proto.reports) != 0 {
  271. t.Errorf("reports = %v, want no write", proto.reports)
  272. }
  273. }