color_verify_test.go 9.8 KB

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