color_verify_test.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. package main
  2. import (
  3. "bytes"
  4. "errors"
  5. "strings"
  6. "testing"
  7. intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
  8. "netdome.biz/paul/qmk-rgb/internal/via"
  9. )
  10. // colorProtocol reports the two byte color the keyboard holds for the color
  11. // value ID, and the single byte brightness for the brightness value ID, so a
  12. // test can describe a keyboard that applied something other than the request.
  13. type colorProtocol struct {
  14. colorWrites []colorWrite
  15. brightnessWrites []uint8
  16. appliedColor map[via.Channel][2]uint8
  17. appliedBright map[via.Channel]uint8
  18. shortColorRead bool
  19. setColorErr error
  20. getErr error
  21. }
  22. type colorWrite struct {
  23. channel via.Channel
  24. hue uint8
  25. sat uint8
  26. }
  27. func (p *colorProtocol) SetValue(_ via.Channel, param, value uint8) error {
  28. if param == uint8(intrgb.Brightness) {
  29. p.brightnessWrites = append(p.brightnessWrites, value)
  30. }
  31. return nil
  32. }
  33. func (p *colorProtocol) SetColor(channel via.Channel, hue, sat uint8) error {
  34. if p.setColorErr != nil {
  35. return p.setColorErr
  36. }
  37. p.colorWrites = append(p.colorWrites, colorWrite{channel: channel, hue: hue, sat: sat})
  38. return nil
  39. }
  40. func (p *colorProtocol) GetValue(channel via.Channel, param uint8) ([]byte, error) {
  41. if p.getErr != nil {
  42. return nil, p.getErr
  43. }
  44. if param == uint8(intrgb.ColorValue) {
  45. if p.shortColorRead {
  46. return []byte{0}, nil
  47. }
  48. c := p.appliedColor[channel]
  49. return []byte{c[0], c[1]}, nil
  50. }
  51. return []byte{p.appliedBright[channel]}, nil
  52. }
  53. func (p *colorProtocol) Close() error { return nil }
  54. // A color is two bytes on the wire, so the read-back has to look at both. A
  55. // one byte answer would leave the saturation unverified.
  56. func TestSetColorVerifiedReadsBackHueAndSaturation(t *testing.T) {
  57. proto := &colorProtocol{
  58. appliedColor: map[via.Channel][2]uint8{via.ChannelRgblight: {99, 255}},
  59. }
  60. results, err := setColorVerified(proto, []intrgb.Zone{intrgb.ZoneLogo}, 85, 255)
  61. if err != nil {
  62. t.Fatalf("setColorVerified() error = %v", err)
  63. }
  64. if len(results) != 1 {
  65. t.Fatalf("results = %d entries, want 1", len(results))
  66. }
  67. got := results[0]
  68. if got.Zone != intrgb.ZoneLogo {
  69. t.Errorf("results[0].Zone = %q, want %q", got.Zone, intrgb.ZoneLogo)
  70. }
  71. if got.RequestedHue != 85 || got.RequestedSaturation != 255 {
  72. t.Errorf("requested = hue %d sat %d, want hue 85 sat 255", got.RequestedHue, got.RequestedSaturation)
  73. }
  74. if got.Hue != 99 || got.Saturation != 255 {
  75. t.Errorf("applied = hue %d sat %d, want hue 99 sat 255", got.Hue, got.Saturation)
  76. }
  77. if !got.Mismatch() {
  78. t.Error("Mismatch() = false, want true when the keyboard stored another hue")
  79. }
  80. }
  81. func TestSetColorVerifiedReportsNoMismatchWhenKeyboardAppliesTheRequest(t *testing.T) {
  82. proto := &colorProtocol{
  83. appliedColor: map[via.Channel][2]uint8{via.ChannelRgblight: {0, 255}},
  84. }
  85. results, err := setColorVerified(proto, []intrgb.Zone{intrgb.ZoneLogo}, 0, 255)
  86. if err != nil {
  87. t.Fatalf("setColorVerified() error = %v", err)
  88. }
  89. if results[0].Mismatch() {
  90. t.Error("Mismatch() = true, want false when the keyboard applied the request")
  91. }
  92. }
  93. // A truncated answer means the keyboard did not report both components, so the
  94. // command must not claim it knows the applied color.
  95. func TestSetColorVerifiedRejectsATruncatedColorRead(t *testing.T) {
  96. proto := &colorProtocol{shortColorRead: true}
  97. _, err := setColorVerified(proto, []intrgb.Zone{intrgb.ZoneLogo}, 0, 255)
  98. if err == nil {
  99. t.Fatal("setColorVerified() expected an error, got nil")
  100. }
  101. if !strings.Contains(err.Error(), "logo") {
  102. t.Errorf("error = %q, want it to name the zone whose read back was unusable", err)
  103. }
  104. }
  105. func TestSetColorVerifiedPropagatesWriteErrors(t *testing.T) {
  106. proto := &colorProtocol{setColorErr: errors.New("write refused")}
  107. _, err := setColorVerified(proto, []intrgb.Zone{intrgb.ZoneLogo}, 0, 255)
  108. if err == nil {
  109. t.Fatal("setColorVerified() expected an error, got nil")
  110. }
  111. if !strings.Contains(err.Error(), "write refused") {
  112. t.Errorf("error = %q, want the keyboard's own reason", err)
  113. }
  114. }
  115. func runColor(t *testing.T, proto *colorProtocol, zoneFlag string, args ...string) (stdout, stderr string, err error) {
  116. t.Helper()
  117. originalOpen := openRGBProtocol
  118. originalZone := targetZone
  119. t.Cleanup(func() {
  120. openRGBProtocol = originalOpen
  121. targetZone = originalZone
  122. })
  123. openRGBProtocol = func() (rgbProtocol, error) { return proto, nil }
  124. targetZone = zoneFlag
  125. var out, errOut bytes.Buffer
  126. cmd := NewColorCmd()
  127. cmd.SetOut(&out)
  128. cmd.SetErr(&errOut)
  129. cmd.SetArgs(args)
  130. err = cmd.Execute()
  131. return out.String(), errOut.String(), err
  132. }
  133. func allZonesAppliedColor(hue, sat uint8) map[via.Channel][2]uint8 {
  134. return map[via.Channel][2]uint8{
  135. via.ChannelRgblight: {hue, sat},
  136. via.ChannelRgbMatrix: {hue, sat},
  137. via.ChannelAudio: {hue, sat},
  138. }
  139. }
  140. func TestColorReportsTheAppliedHueAndSaturation(t *testing.T) {
  141. // 00ff00 is hue 85 at full saturation.
  142. proto := &colorProtocol{appliedColor: allZonesAppliedColor(85, 255)}
  143. stdout, stderr, err := runColor(t, proto, "logo", "00ff00")
  144. if err != nil {
  145. t.Fatalf("color returned error: %v", err)
  146. }
  147. if strings.TrimSpace(stdout) != "Color set to hue 85 sat 255" {
  148. t.Errorf("stdout = %q, want the exact success message", stdout)
  149. }
  150. if stderr != "" {
  151. t.Errorf("stderr = %q, want nothing on stderr when the request was met", stderr)
  152. }
  153. }
  154. // The hex notations carry no brightness, so the command must not write one and
  155. // must not report one.
  156. func TestColorHexNotationLeavesBrightnessAlone(t *testing.T) {
  157. proto := &colorProtocol{appliedColor: allZonesAppliedColor(0, 255)}
  158. stdout, _, err := runColor(t, proto, "", "rgb:ff0000")
  159. if err != nil {
  160. t.Fatalf("color returned error: %v", err)
  161. }
  162. if len(proto.brightnessWrites) != 0 {
  163. t.Errorf("brightness writes = %v, want none for a hex notation", proto.brightnessWrites)
  164. }
  165. if strings.Contains(stdout, "brightness") {
  166. t.Errorf("stdout = %q, must not report a brightness the command did not set", stdout)
  167. }
  168. }
  169. // hsv: carries a value, and the keyboard has no value register for it, so the
  170. // value goes to the brightness of the same zones.
  171. func TestColorHSVNotationWritesTheValueAsBrightness(t *testing.T) {
  172. proto := &colorProtocol{
  173. appliedColor: allZonesAppliedColor(85, 255),
  174. appliedBright: map[via.Channel]uint8{via.ChannelRgblight: 200, via.ChannelRgbMatrix: 200, via.ChannelAudio: 200},
  175. }
  176. stdout, _, err := runColor(t, proto, "", "hsv:85,255,200")
  177. if err != nil {
  178. t.Fatalf("color returned error: %v", err)
  179. }
  180. if len(proto.brightnessWrites) != 3 {
  181. t.Fatalf("brightness writes = %v, want one per selected zone", proto.brightnessWrites)
  182. }
  183. for _, v := range proto.brightnessWrites {
  184. if v != 200 {
  185. t.Errorf("brightness write = %d, want 200", v)
  186. }
  187. }
  188. if strings.TrimSpace(stdout) != "Color set to hue 85 sat 255 brightness 200" {
  189. t.Errorf("stdout = %q, want hue, saturation and brightness", stdout)
  190. }
  191. }
  192. // The Impact 80 clamps brightness at 160 on logo and side and scales it up on
  193. // the backlight channel, so an hsv: value is reported the way brightness is.
  194. func TestColorHSVSummarisesTheBrightnessTheKeyboardApplied(t *testing.T) {
  195. proto := &colorProtocol{
  196. appliedColor: allZonesAppliedColor(0, 255),
  197. appliedBright: map[via.Channel]uint8{via.ChannelRgblight: 160, via.ChannelRgbMatrix: 255, via.ChannelAudio: 160},
  198. }
  199. stdout, _, err := runColor(t, proto, "", "hsv:0,255,200")
  200. if err != nil {
  201. t.Fatalf("color returned error: %v", err)
  202. }
  203. for _, want := range []string{
  204. "logo hue 0 sat 255 brightness 160",
  205. "backlight hue 0 sat 255 brightness 255",
  206. "side hue 0 sat 255 brightness 160",
  207. "requested hue 0 sat 255 brightness 200",
  208. } {
  209. if !strings.Contains(stdout, want) {
  210. t.Errorf("stdout = %q, want it to contain %q", stdout, want)
  211. }
  212. }
  213. if strings.Contains(stdout, "set to") {
  214. t.Errorf("stdout = %q, must not claim the request was met when a zone clamped it", stdout)
  215. }
  216. if strings.Count(strings.TrimSpace(stdout), "\n") != 0 {
  217. t.Errorf("stdout = %q, want exactly one line", stdout)
  218. }
  219. }
  220. // A notation the tool cannot parse must fail before the keyboard is opened.
  221. func TestColorRejectsAnUnknownNotationWithoutOpeningTheDevice(t *testing.T) {
  222. proto := &colorProtocol{}
  223. opened := false
  224. originalOpen := openRGBProtocol
  225. originalZone := targetZone
  226. t.Cleanup(func() {
  227. openRGBProtocol = originalOpen
  228. targetZone = originalZone
  229. })
  230. openRGBProtocol = func() (rgbProtocol, error) {
  231. opened = true
  232. return proto, nil
  233. }
  234. targetZone = ""
  235. var out bytes.Buffer
  236. cmd := NewColorCmd()
  237. cmd.SetOut(&out)
  238. cmd.SetErr(&out)
  239. cmd.SetArgs([]string{"xyz:1"})
  240. err := cmd.Execute()
  241. if err == nil {
  242. t.Fatal("color expected an error, got nil")
  243. }
  244. if opened {
  245. t.Error("the keyboard was opened, want the notation rejected first")
  246. }
  247. if len(proto.colorWrites) != 0 {
  248. t.Errorf("color writes = %v, want none", proto.colorWrites)
  249. }
  250. }