channels_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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"}, nil)
  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"}, nil)
  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"}, nil)
  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, nil); err == nil {
  40. t.Fatal("resolveZoneName(\"logo\", nil, 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"}, nil)
  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. They come from that board's
  92. // definition file, which is where a channel's name lives now.
  93. func TestPrepareTargetUsesTheSelectedKeyboard(t *testing.T) {
  94. devices := []intdevice.Device{
  95. {VendorID: 0x6666, ProductID: 0x0001},
  96. {VendorID: 0x36B0, ProductID: 0x309F, Name: "Impact 80"},
  97. }
  98. dir := t.TempDir()
  99. writeDefinition(t, dir, "rainy75.json", `{
  100. "name": "Rainy 75", "vendorId": "0x6666", "productId": "0x0001",
  101. "menus": [{"label":"Lighting","content":[{"label":"deck","content":[
  102. {"label":"Effect","type":"dropdown","content":["id_qmk_rgblight_effect",2,2],"options":["none"]}]}]}]}`)
  103. writeDefinition(t, dir, "impact80.json", `{
  104. "name": "Impact 80", "vendorId": "0x36B0", "productId": "0x309F",
  105. "menus": [{"label":"Lighting","content":[{"label":"logo","content":[
  106. {"label":"Effect","type":"dropdown","content":["id_qmk_rgblight_effect",2,2],"options":["none","wave"]}]}]}]}`)
  107. t.Cleanup(forceDefinitionsDir(t, dir))
  108. originalDiscover := discoverAll
  109. originalTarget := targetDevice
  110. originalZone := targetZone
  111. t.Cleanup(func() {
  112. discoverAll = originalDiscover
  113. targetDevice = originalTarget
  114. targetZone = originalZone
  115. })
  116. discoverAll = func() ([]intdevice.Device, error) { return devices, nil }
  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. if got.Device.Name != "Impact 80" {
  127. t.Errorf("name = %q, want the name the definition gives the board", got.Device.Name)
  128. }
  129. }
  130. // A board renamed after a profile was written leaves keys that resolve to
  131. // nothing. Each one must be reported by name, and nothing may be written.
  132. func TestLoadWarnsForEveryUnresolvableZoneKey(t *testing.T) {
  133. dir := t.TempDir()
  134. profile := Profile{
  135. Name: "renamed",
  136. Version: 1,
  137. Zones: map[string]*ZoneSettings{
  138. "old-logo": {Enabled: true, Effect: "light", Brightness: 100, Speed: 1, Color: "00ff"},
  139. "old-backlight": {Enabled: true, Effect: "wave", Brightness: 100, Speed: 1, Color: "00ff"},
  140. },
  141. }
  142. originalDir := profilesDir
  143. t.Cleanup(func() { profilesDir = originalDir })
  144. profilesDir = dir
  145. if err := profile.Save(); err != nil {
  146. t.Fatalf("save profile: %v", err)
  147. }
  148. proto := &verifyingProtocol{}
  149. t.Cleanup(stubTargetForProfileTest(t, proto, "", impact80Display()))
  150. var stderr bytes.Buffer
  151. cmd := NewProfileLoadCmd()
  152. cmd.SetOut(&bytes.Buffer{})
  153. cmd.SetErr(&stderr)
  154. cmd.SetArgs([]string{"renamed"})
  155. if err := cmd.Execute(); err != nil {
  156. t.Fatalf("load returned error: %v", err)
  157. }
  158. for _, want := range []string{"old-logo", "old-backlight"} {
  159. if !strings.Contains(stderr.String(), want) {
  160. t.Errorf("stderr = %q, want it to name the unresolved key %q", stderr.String(), want)
  161. }
  162. }
  163. if len(proto.reports) != 0 {
  164. t.Errorf("reports = %v, want nothing written for unresolvable keys", proto.reports)
  165. }
  166. }
  167. // stubTargetForProfileTest points the profile commands at one protocol, one
  168. // profile directory and one --zone value, and returns the restore function.
  169. func stubTargetForProfileTest(t *testing.T, proto rgbProtocol, zoneFlag string, display map[uint16]string) func() {
  170. t.Helper()
  171. originalTarget := openTarget
  172. originalZone := targetZone
  173. openTarget = func() (rgbProtocol, targetDeviceData, []via.Channel, error) {
  174. // The board identity matters: the catalog is selected by VID/PID, and a
  175. // profile that names an effect needs one to resolve it against.
  176. target := targetDeviceData{
  177. Device: intdevice.Device{VendorID: 0x36B0, ProductID: 0x309F},
  178. Display: display,
  179. }
  180. requested, err := resolveZoneName(zoneFlag, display, nil)
  181. if err != nil {
  182. return nil, target, nil, err
  183. }
  184. target.Requested = requested
  185. resolved, err := resolveChannels(proto, target)
  186. if err != nil {
  187. return nil, target, nil, err
  188. }
  189. return proto, target, resolved, nil
  190. }
  191. targetZone = zoneFlag
  192. return func() {
  193. openTarget = originalTarget
  194. targetZone = originalZone
  195. }
  196. }
  197. // A key that names a channel this keyboard does not have must be reported like
  198. // any other key it cannot place. "backlight" is the case that matters: it is a
  199. // display name on the Impact 80 and QMK's subsystem name for channel 1, so it
  200. // still resolves after a board renames channel 3 — to a channel that is absent.
  201. func TestLoadWarnsWhenAKeyNamesAnAbsentChannel(t *testing.T) {
  202. dir := t.TempDir()
  203. originalDir := profilesDir
  204. t.Cleanup(func() { profilesDir = originalDir })
  205. profilesDir = dir
  206. profile := Profile{
  207. Name: "renamed",
  208. Version: 1,
  209. Zones: map[string]*ZoneSettings{
  210. "backlight": {Enabled: true, Effect: "light", Brightness: 100, Speed: 1, Color: "00ff"},
  211. },
  212. }
  213. if err := profile.Save(); err != nil {
  214. t.Fatalf("save profile: %v", err)
  215. }
  216. proto := &verifyingProtocol{}
  217. t.Cleanup(stubTargetForProfileTest(t, proto, "", nil))
  218. var stderr bytes.Buffer
  219. cmd := NewProfileLoadCmd()
  220. cmd.SetOut(&bytes.Buffer{})
  221. cmd.SetErr(&stderr)
  222. cmd.SetArgs([]string{"renamed"})
  223. if err := cmd.Execute(); err != nil {
  224. t.Fatalf("load returned error: %v", err)
  225. }
  226. if !strings.Contains(stderr.String(), "backlight") {
  227. t.Errorf("stderr = %q, want the key reported by name", stderr.String())
  228. }
  229. if len(proto.reports) != 0 {
  230. t.Errorf("reports = %v, want nothing written for an absent channel", proto.reports)
  231. }
  232. }
  233. // Without a catalog there are no effect names to look the profile's value up
  234. // in. The load must say that, not claim the name was not found.
  235. func TestLoadSaysSoWhenTheBoardHasNoCatalog(t *testing.T) {
  236. dir := t.TempDir()
  237. originalDir := profilesDir
  238. t.Cleanup(func() { profilesDir = originalDir })
  239. profilesDir = dir
  240. profile := Profile{
  241. Name: "p",
  242. Version: 1,
  243. Zones: map[string]*ZoneSettings{
  244. "rgb_matrix": {Enabled: true, Effect: "breathing", Brightness: 100, Speed: 1, Color: "00ff"},
  245. },
  246. }
  247. if err := profile.Save(); err != nil {
  248. t.Fatalf("save profile: %v", err)
  249. }
  250. proto := &verifyingProtocol{applied: map[via.Channel]uint8{
  251. via.ChannelRgblight: 160,
  252. via.ChannelRgbMatrix: 255,
  253. via.ChannelAudio: 160,
  254. }}
  255. restore := stubTargetForUnknownBoard(t, proto, dir)
  256. t.Cleanup(restore)
  257. var stderr bytes.Buffer
  258. cmd := NewProfileLoadCmd()
  259. cmd.SetOut(&bytes.Buffer{})
  260. cmd.SetErr(&stderr)
  261. cmd.SetArgs([]string{"p"})
  262. if err := cmd.Execute(); err != nil {
  263. t.Fatalf("load returned error: %v", err)
  264. }
  265. if !strings.Contains(stderr.String(), "catalog") {
  266. t.Errorf("stderr = %q, want the missing catalog named", stderr.String())
  267. }
  268. if strings.Contains(stderr.String(), `effect "breathing" not found`) {
  269. t.Errorf("stderr = %q, must not blame the effect name for a missing catalog", stderr.String())
  270. }
  271. if len(proto.reports) != 0 {
  272. t.Errorf("reports = %v, want nothing written without a catalog", proto.reports)
  273. }
  274. }
  275. // stubTargetForUnknownBoard points the commands at a board that has channels but
  276. // no definition and no compiled-in catalog, so it has no effect names.
  277. func stubTargetForUnknownBoard(t *testing.T, proto rgbProtocol, dir string) func() {
  278. t.Helper()
  279. originalTarget := openTarget
  280. originalDir := profilesDir
  281. openTarget = func() (rgbProtocol, targetDeviceData, []via.Channel, error) {
  282. return proto, targetDeviceData{
  283. Device: intdevice.Device{VendorID: 0x6666, ProductID: 0x0001},
  284. }, impact80Channels(), nil
  285. }
  286. profilesDir = dir
  287. return func() {
  288. openTarget = originalTarget
  289. profilesDir = originalDir
  290. }
  291. }
  292. // A board without a catalog cannot name its effects, so a saved profile records
  293. // "unknown" for every channel and cannot be loaded back. That is worth saying at
  294. // save time, where the user can still do something about it.
  295. func TestSaveWarnsThatEffectNamesCannotBeRecorded(t *testing.T) {
  296. dir := t.TempDir()
  297. originalDir := profilesDir
  298. t.Cleanup(func() { profilesDir = originalDir })
  299. profilesDir = dir
  300. proto := &verifyingProtocol{applied: map[via.Channel]uint8{
  301. via.ChannelRgblight: 160,
  302. via.ChannelRgbMatrix: 255,
  303. via.ChannelAudio: 160,
  304. }}
  305. restore := stubTargetForUnknownBoard(t, proto, dir)
  306. t.Cleanup(restore)
  307. var stderr bytes.Buffer
  308. cmd := NewProfileSaveCmd()
  309. cmd.SetOut(&bytes.Buffer{})
  310. cmd.SetErr(&stderr)
  311. cmd.SetArgs([]string{"p"})
  312. if err := cmd.Execute(); err != nil {
  313. t.Fatalf("save returned error: %v", err)
  314. }
  315. if !strings.Contains(stderr.String(), "unknown") {
  316. t.Errorf("stderr = %q, want the effect name it cannot record named", stderr.String())
  317. }
  318. if !strings.Contains(stderr.String(), "catalog") {
  319. t.Errorf("stderr = %q, want the missing catalog named", stderr.String())
  320. }
  321. }
  322. // A channel's own definition label and the subsystem name it replaces can both
  323. // match one name, and that is one channel rather than a conflict: the
  324. // documented `--zone backlight` has to keep working on a board whose definition
  325. // calls the channel "Backlight".
  326. func TestResolveZoneNameAcceptsTheNameADefinitionReplaced(t *testing.T) {
  327. display := map[uint16]string{2: "logo", 3: "Backlight", 4: "side"}
  328. alternatives := map[uint16][]string{3: {"backlight"}}
  329. for _, name := range []string{"backlight", "Backlight", "BACKLIGHT"} {
  330. got, err := resolveZoneName(name, display, alternatives)
  331. if err != nil {
  332. t.Errorf("resolveZoneName(%q) error = %v", name, err)
  333. continue
  334. }
  335. if len(got) != 1 || got[0] != 3 {
  336. t.Errorf("resolveZoneName(%q) = %v, want [3]", name, got)
  337. }
  338. }
  339. }
  340. // Two different channels answering to one name is still a conflict, whichever
  341. // source the names come from.
  342. func TestResolveZoneNameStillReportsARealConflict(t *testing.T) {
  343. display := map[uint16]string{2: "Backlight", 3: "backlight"}
  344. if _, err := resolveZoneName("backlight", display, map[uint16][]string{4: {"backlight"}}); err == nil {
  345. t.Error("resolveZoneName() = nil error, want a conflict for one name on two channels")
  346. }
  347. }