channels_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. package main
  2. import (
  3. "bytes"
  4. "strings"
  5. "testing"
  6. intdevice "netdome.biz/paul/qmk-rgb/internal/device"
  7. "netdome.biz/paul/qmk-rgb/internal/via"
  8. )
  9. func TestResolveZoneNameAcceptsASubsystemName(t *testing.T) {
  10. got, err := resolveZoneName("rgb_matrix", map[uint16]string{2: "logo"})
  11. if err != nil {
  12. t.Fatalf("resolveZoneName() error = %v", err)
  13. }
  14. if len(got) != 1 || got[0] != via.ChannelRgbMatrix {
  15. t.Errorf("resolveZoneName() = %v, want [3]", got)
  16. }
  17. }
  18. func TestResolveZoneNameAcceptsADisplayName(t *testing.T) {
  19. got, err := resolveZoneName("logo", map[uint16]string{2: "logo", 3: "backlight", 4: "side"})
  20. if err != nil {
  21. t.Fatalf("resolveZoneName() error = %v", err)
  22. }
  23. if len(got) != 1 || got[0] != via.ChannelRgblight {
  24. t.Errorf("resolveZoneName() = %v, want [2]", got)
  25. }
  26. }
  27. func TestResolveZoneNameRejectsAnUnknownName(t *testing.T) {
  28. _, err := resolveZoneName("nope", map[uint16]string{2: "logo"})
  29. if err == nil {
  30. t.Fatal("resolveZoneName() expected an error, got nil")
  31. }
  32. if !strings.Contains(err.Error(), "rgb_matrix") {
  33. t.Errorf("error = %q, want it to name an accepted form", err)
  34. }
  35. }
  36. // A board that supplies no display names keeps the subsystem vocabulary, so the
  37. // physical names that work on the Impact 80 do not work elsewhere.
  38. func TestResolveZoneNameIgnoresDisplayNamesForAnotherBoard(t *testing.T) {
  39. if _, err := resolveZoneName("logo", nil); err == nil {
  40. t.Fatal("resolveZoneName(\"logo\", nil) expected an error, got nil")
  41. }
  42. }
  43. // Two channels carrying the same display name are ambiguous, and the tool
  44. // reports that rather than picking one. Whether a display name shadows another
  45. // channel's subsystem name cannot be decided here, because presence is only
  46. // known after the probe; displayNameConflicts decides that.
  47. func TestResolveZoneNameReportsAnAmbiguousName(t *testing.T) {
  48. _, err := resolveZoneName("backlight", map[uint16]string{2: "logo", 3: "backlight", 4: "backlight"})
  49. if err == nil {
  50. t.Fatal("resolveZoneName() expected an error for a name two channels answer to")
  51. }
  52. if !strings.Contains(err.Error(), "backlight") {
  53. t.Errorf("error = %q, want it to name the conflicting name", err)
  54. }
  55. }
  56. func TestDisplayNameConflictsRejectsAShadowedSubsystemName(t *testing.T) {
  57. // Channel 1 is present and its subsystem is "backlight", while the file
  58. // also calls channel 3 "backlight": two channels, one name.
  59. err := displayNameConflicts(
  60. map[uint16]string{3: "backlight"},
  61. []via.Channel{via.ChannelBacklight, via.ChannelRgbMatrix},
  62. )
  63. if err == nil {
  64. t.Fatal("displayNameConflicts() expected an error, got nil")
  65. }
  66. if !strings.Contains(err.Error(), "backlight") {
  67. t.Errorf("error = %q, want it to name the conflicting name", err)
  68. }
  69. }
  70. func TestDisplayNameConflictsAllowsTheImpact80Naming(t *testing.T) {
  71. // The Impact 80 calls channel 3 "backlight" and has no channel 1, so
  72. // nothing shadows anything.
  73. err := displayNameConflicts(
  74. map[uint16]string{2: "logo", 3: "backlight", 4: "side"},
  75. []via.Channel{via.ChannelRgblight, via.ChannelRgbMatrix, via.ChannelAudio},
  76. )
  77. if err != nil {
  78. t.Fatalf("displayNameConflicts() error = %v, want nil", err)
  79. }
  80. }
  81. func TestChannelNamePrefersTheDisplayName(t *testing.T) {
  82. display := map[uint16]string{2: "logo"}
  83. if got := channelName(via.ChannelRgblight, display); got != "logo" {
  84. t.Errorf("channelName(2) = %q, want %q", got, "logo")
  85. }
  86. if got := channelName(via.ChannelRgbMatrix, display); got != "rgb_matrix" {
  87. t.Errorf("channelName(3) = %q, want %q", got, "rgb_matrix")
  88. }
  89. }
  90. // The display names must come from the keyboard the command targets, not from
  91. // whichever one enumeration returned first.
  92. func TestPrepareTargetUsesTheSelectedKeyboard(t *testing.T) {
  93. devices := []intdevice.Device{
  94. {VendorID: 0x6666, ProductID: 0x0001},
  95. {VendorID: 0x36B0, ProductID: 0x309F, Name: "Wobkey Impact 80"},
  96. }
  97. originalDiscover := discoverAll
  98. originalKeyboardFor := keyboardFor
  99. originalTarget := targetDevice
  100. originalZone := targetZone
  101. t.Cleanup(func() {
  102. discoverAll = originalDiscover
  103. keyboardFor = originalKeyboardFor
  104. targetDevice = originalTarget
  105. targetZone = originalZone
  106. })
  107. discoverAll = func() ([]intdevice.Device, error) { return devices, nil }
  108. keyboardFor = func(vendorID, productID uint16) (intdevice.Keyboard, bool, error) {
  109. switch {
  110. case vendorID == 0x6666:
  111. return intdevice.Keyboard{Name: "Wobkey Rainy 75", Channels: map[uint16]string{2: "deck"}}, true, nil
  112. case vendorID == 0x36B0:
  113. return intdevice.Keyboard{Name: "Wobkey Impact 80", Channels: map[uint16]string{2: "logo"}}, true, nil
  114. }
  115. return intdevice.Keyboard{}, false, nil
  116. }
  117. targetDevice = "2"
  118. targetZone = "logo"
  119. got, err := prepareTarget()
  120. if err != nil {
  121. t.Fatalf("prepareTarget() error = %v", err)
  122. }
  123. if got.Display[2] != "logo" {
  124. t.Errorf("display names = %v, want the Impact 80's", got.Display)
  125. }
  126. }
  127. // A board renamed after a profile was written leaves keys that resolve to
  128. // nothing. Each one must be reported by name, and nothing may be written.
  129. func TestLoadWarnsForEveryUnresolvableZoneKey(t *testing.T) {
  130. dir := t.TempDir()
  131. profile := Profile{
  132. Name: "renamed",
  133. Version: 1,
  134. Zones: map[string]*ZoneSettings{
  135. "old-logo": {Enabled: true, Effect: "light", Brightness: 100, Speed: 1, Color: "00ff"},
  136. "old-backlight": {Enabled: true, Effect: "wave", Brightness: 100, Speed: 1, Color: "00ff"},
  137. },
  138. }
  139. originalDir := profilesDir
  140. t.Cleanup(func() { profilesDir = originalDir })
  141. profilesDir = dir
  142. if err := profile.Save(); err != nil {
  143. t.Fatalf("save profile: %v", err)
  144. }
  145. proto := &verifyingProtocol{}
  146. t.Cleanup(stubTargetForProfileTest(t, proto, "", impact80Display()))
  147. var stderr bytes.Buffer
  148. cmd := NewProfileLoadCmd()
  149. cmd.SetOut(&bytes.Buffer{})
  150. cmd.SetErr(&stderr)
  151. cmd.SetArgs([]string{"renamed"})
  152. if err := cmd.Execute(); err != nil {
  153. t.Fatalf("load returned error: %v", err)
  154. }
  155. for _, want := range []string{"old-logo", "old-backlight"} {
  156. if !strings.Contains(stderr.String(), want) {
  157. t.Errorf("stderr = %q, want it to name the unresolved key %q", stderr.String(), want)
  158. }
  159. }
  160. if len(proto.reports) != 0 {
  161. t.Errorf("reports = %v, want nothing written for unresolvable keys", proto.reports)
  162. }
  163. }
  164. // stubTargetForProfileTest points the profile commands at one protocol, one
  165. // profile directory and one --zone value, and returns the restore function.
  166. func stubTargetForProfileTest(t *testing.T, proto rgbProtocol, zoneFlag string, display map[uint16]string) func() {
  167. t.Helper()
  168. originalTarget := openTarget
  169. originalZone := targetZone
  170. openTarget = func() (rgbProtocol, targetDeviceData, []via.Channel, error) {
  171. // The board identity matters: the catalog is selected by VID/PID, and a
  172. // profile that names an effect needs one to resolve it against.
  173. target := targetDeviceData{
  174. Device: intdevice.Device{VendorID: 0x36B0, ProductID: 0x309F},
  175. Display: display,
  176. }
  177. requested, err := resolveZoneName(zoneFlag, display)
  178. if err != nil {
  179. return nil, target, nil, err
  180. }
  181. target.Requested = requested
  182. resolved, err := resolveChannels(proto, target)
  183. if err != nil {
  184. return nil, target, nil, err
  185. }
  186. return proto, target, resolved, nil
  187. }
  188. targetZone = zoneFlag
  189. return func() {
  190. openTarget = originalTarget
  191. targetZone = originalZone
  192. }
  193. }
  194. // A key that names a channel this keyboard does not have must be reported like
  195. // any other key it cannot place. "backlight" is the case that matters: it is a
  196. // display name on the Impact 80 and QMK's subsystem name for channel 1, so it
  197. // still resolves after a board renames channel 3 — to a channel that is absent.
  198. func TestLoadWarnsWhenAKeyNamesAnAbsentChannel(t *testing.T) {
  199. dir := t.TempDir()
  200. originalDir := profilesDir
  201. t.Cleanup(func() { profilesDir = originalDir })
  202. profilesDir = dir
  203. profile := Profile{
  204. Name: "renamed",
  205. Version: 1,
  206. Zones: map[string]*ZoneSettings{
  207. "backlight": {Enabled: true, Effect: "light", Brightness: 100, Speed: 1, Color: "00ff"},
  208. },
  209. }
  210. if err := profile.Save(); err != nil {
  211. t.Fatalf("save profile: %v", err)
  212. }
  213. proto := &verifyingProtocol{}
  214. t.Cleanup(stubTargetForProfileTest(t, proto, "", nil))
  215. var stderr bytes.Buffer
  216. cmd := NewProfileLoadCmd()
  217. cmd.SetOut(&bytes.Buffer{})
  218. cmd.SetErr(&stderr)
  219. cmd.SetArgs([]string{"renamed"})
  220. if err := cmd.Execute(); err != nil {
  221. t.Fatalf("load returned error: %v", err)
  222. }
  223. if !strings.Contains(stderr.String(), "backlight") {
  224. t.Errorf("stderr = %q, want the key reported by name", stderr.String())
  225. }
  226. if len(proto.reports) != 0 {
  227. t.Errorf("reports = %v, want nothing written for an absent channel", proto.reports)
  228. }
  229. }
  230. // Without a catalog there are no effect names to look the profile's value up
  231. // in. The load must say that, not claim the name was not found.
  232. func TestLoadSaysSoWhenTheBoardHasNoCatalog(t *testing.T) {
  233. dir := t.TempDir()
  234. originalDir := profilesDir
  235. t.Cleanup(func() { profilesDir = originalDir })
  236. profilesDir = dir
  237. profile := Profile{
  238. Name: "p",
  239. Version: 1,
  240. Zones: map[string]*ZoneSettings{
  241. "rgb_matrix": {Enabled: true, Effect: "breathing", Brightness: 100, Speed: 1, Color: "00ff"},
  242. },
  243. }
  244. if err := profile.Save(); err != nil {
  245. t.Fatalf("save profile: %v", err)
  246. }
  247. proto := &verifyingProtocol{}
  248. restore := stubTargetForUnknownBoard(t, proto, dir)
  249. t.Cleanup(restore)
  250. var stderr bytes.Buffer
  251. cmd := NewProfileLoadCmd()
  252. cmd.SetOut(&bytes.Buffer{})
  253. cmd.SetErr(&stderr)
  254. cmd.SetArgs([]string{"p"})
  255. if err := cmd.Execute(); err != nil {
  256. t.Fatalf("load returned error: %v", err)
  257. }
  258. if !strings.Contains(stderr.String(), "catalog") {
  259. t.Errorf("stderr = %q, want the missing catalog named", stderr.String())
  260. }
  261. if strings.Contains(stderr.String(), `effect "breathing" not found`) {
  262. t.Errorf("stderr = %q, must not blame the effect name for a missing catalog", stderr.String())
  263. }
  264. if len(proto.reports) != 0 {
  265. t.Errorf("reports = %v, want nothing written without a catalog", proto.reports)
  266. }
  267. }
  268. // stubTargetForUnknownBoard points the commands at a board that has channels but
  269. // no keyboards.json entry, so it has no catalog.
  270. func stubTargetForUnknownBoard(t *testing.T, proto rgbProtocol, dir string) func() {
  271. t.Helper()
  272. originalTarget := openTarget
  273. originalDir := profilesDir
  274. openTarget = func() (rgbProtocol, targetDeviceData, []via.Channel, error) {
  275. return proto, targetDeviceData{
  276. Device: intdevice.Device{VendorID: 0x6666, ProductID: 0x0001},
  277. }, impact80Channels(), nil
  278. }
  279. profilesDir = dir
  280. return func() {
  281. openTarget = originalTarget
  282. profilesDir = originalDir
  283. }
  284. }