channels_test.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. package main
  2. import (
  3. "bytes"
  4. "reflect"
  5. "strings"
  6. "testing"
  7. intdevice "netdome.biz/paul/qmk-rgb/internal/device"
  8. "netdome.biz/paul/qmk-rgb/internal/via"
  9. )
  10. func TestResolveZoneNameAcceptsASubsystemName(t *testing.T) {
  11. got, err := resolveZoneName("rgb_matrix", map[uint16]string{2: "logo"}, nil)
  12. if err != nil {
  13. t.Fatalf("resolveZoneName() error = %v", err)
  14. }
  15. if len(got) != 1 || got[0] != via.ChannelRgbMatrix {
  16. t.Errorf("resolveZoneName() = %v, want [3]", got)
  17. }
  18. }
  19. func TestResolveZoneNameAcceptsADisplayName(t *testing.T) {
  20. got, err := resolveZoneName("logo", map[uint16]string{2: "logo", 3: "backlight", 4: "side"}, nil)
  21. if err != nil {
  22. t.Fatalf("resolveZoneName() error = %v", err)
  23. }
  24. if len(got) != 1 || got[0] != via.ChannelRgblight {
  25. t.Errorf("resolveZoneName() = %v, want [2]", got)
  26. }
  27. }
  28. func TestResolveZoneNameRejectsAnUnknownName(t *testing.T) {
  29. _, err := resolveZoneName("nope", map[uint16]string{2: "logo"}, nil)
  30. if err == nil {
  31. t.Fatal("resolveZoneName() expected an error, got nil")
  32. }
  33. if !strings.Contains(err.Error(), "rgb_matrix") {
  34. t.Errorf("error = %q, want it to name an accepted form", err)
  35. }
  36. }
  37. // A board that supplies no display names keeps the subsystem vocabulary, so the
  38. // physical names that work on the Impact 80 do not work elsewhere.
  39. func TestResolveZoneNameIgnoresDisplayNamesForAnotherBoard(t *testing.T) {
  40. if _, err := resolveZoneName("logo", nil, nil); err == nil {
  41. t.Fatal("resolveZoneName(\"logo\", nil, nil) expected an error, got nil")
  42. }
  43. }
  44. // Two channels carrying the same display name are ambiguous, and the tool
  45. // reports that rather than picking one. Whether a display name shadows another
  46. // channel's subsystem name cannot be decided here, because presence is only
  47. // known after the probe; displayNameConflicts decides that.
  48. func TestResolveZoneNameReportsAnAmbiguousName(t *testing.T) {
  49. _, err := resolveZoneName("backlight", map[uint16]string{2: "logo", 3: "backlight", 4: "backlight"}, nil)
  50. if err == nil {
  51. t.Fatal("resolveZoneName() expected an error for a name two channels answer to")
  52. }
  53. if !strings.Contains(err.Error(), "backlight") {
  54. t.Errorf("error = %q, want it to name the conflicting name", err)
  55. }
  56. }
  57. // A zone is one name or several written comma separated, and the channels come
  58. // back in channel order however they were written. A script that parses the
  59. // result sees one order for every spelling of one selection, so the order the
  60. // user typed in is not a thing to depend on.
  61. func TestResolveZoneNameReturnsAChannelOrderWhateverTheSpelling(t *testing.T) {
  62. display := map[uint16]string{2: "logo", 3: "backlight", 4: "side"}
  63. cases := []struct {
  64. zone string
  65. want []via.Channel
  66. }{
  67. {"logo", []via.Channel{via.ChannelRgblight}},
  68. {"logo,side", []via.Channel{via.ChannelRgblight, via.ChannelAudio}},
  69. {"side,logo", []via.Channel{via.ChannelRgblight, via.ChannelAudio}},
  70. {" logo , side ", []via.Channel{via.ChannelRgblight, via.ChannelAudio}},
  71. {"side,logo,backlight", []via.Channel{via.ChannelRgblight, via.ChannelRgbMatrix, via.ChannelAudio}},
  72. // A name twice, and a name that reaches one channel by two of its names,
  73. // are one channel each: a double write is not what was asked for.
  74. {"logo,logo", []via.Channel{via.ChannelRgblight}},
  75. {"logo,rgblight", []via.Channel{via.ChannelRgblight}},
  76. // The QMK subsystem name and the definition's label for the same channel.
  77. {"backlight", []via.Channel{via.ChannelRgbMatrix}},
  78. {"Backlight", []via.Channel{via.ChannelRgbMatrix}},
  79. // A board that renames a channel keeps the subsystem name working.
  80. {"rgblight", []via.Channel{via.ChannelRgblight}},
  81. }
  82. for _, tc := range cases {
  83. t.Run(tc.zone, func(t *testing.T) {
  84. got, err := resolveZoneName(tc.zone, display, map[uint16][]string{3: {"rgb_matrix"}})
  85. if err != nil {
  86. t.Fatalf("resolveZoneName(%q) error = %v", tc.zone, err)
  87. }
  88. if !reflect.DeepEqual(got, tc.want) {
  89. t.Errorf("resolveZoneName(%q) = %v, want %v", tc.zone, got, tc.want)
  90. }
  91. })
  92. }
  93. }
  94. // all is every channel, and naming it beside one more channel is the same
  95. // request as naming it alone. The nil slice is how "every channel" travels: a
  96. // command that only read asks for no zone at all and gets the same thing.
  97. func TestResolveZoneNameTreatsAllAsEveryChannel(t *testing.T) {
  98. display := map[uint16]string{2: "logo", 3: "backlight", 4: "side"}
  99. for _, zone := range []string{"all", "ALL", "All", " all ", "side,all", "all,logo"} {
  100. t.Run(zone, func(t *testing.T) {
  101. got, err := resolveZoneName(zone, display, nil)
  102. if err != nil {
  103. t.Fatalf("resolveZoneName(%q) error = %v", zone, err)
  104. }
  105. if got != nil {
  106. t.Errorf("resolveZoneName(%q) = %v, want nil, which means every channel", zone, got)
  107. }
  108. })
  109. }
  110. }
  111. // A list that names no channel is a typo, and guessing which one was meant writes
  112. // to a channel nobody asked for.
  113. func TestResolveZoneNameRefusesAnEmptyNameInAList(t *testing.T) {
  114. cases := []struct {
  115. zone string
  116. want string
  117. }{
  118. {"logo,", "logo,"},
  119. {",logo", ",logo"},
  120. {"logo,,side", "logo,,side"},
  121. {"logo, ", "logo,"},
  122. {",", ","},
  123. }
  124. for _, tc := range cases {
  125. t.Run(tc.zone, func(t *testing.T) {
  126. _, err := resolveZoneName(tc.zone, map[uint16]string{2: "logo", 4: "side"}, nil)
  127. if err == nil {
  128. t.Fatalf("resolveZoneName(%q) = nil error, want the empty name refused", tc.zone)
  129. }
  130. if !strings.Contains(err.Error(), "empty") {
  131. t.Errorf("error = %q, want it to say the list names no channel", err)
  132. }
  133. if !strings.Contains(err.Error(), tc.want) {
  134. t.Errorf("error = %q, want it to quote %q", err, tc.want)
  135. }
  136. })
  137. }
  138. }
  139. // A name the tool cannot place is reported with the list it came from, because
  140. // "unknown zone" on its own would be a claim about a word the user never wrote.
  141. func TestResolveZoneNameNamesTheListItCameFrom(t *testing.T) {
  142. _, err := resolveZoneName("logo,nope", map[uint16]string{2: "logo"}, nil)
  143. if err == nil {
  144. t.Fatal("resolveZoneName() = nil error, want the unknown name refused")
  145. }
  146. for _, want := range []string{`"nope"`, `in "logo,nope"`, zoneAll} {
  147. if !strings.Contains(err.Error(), want) {
  148. t.Errorf("error = %q, want it to contain %q", err, want)
  149. }
  150. }
  151. }
  152. // One ambiguous name refuses the whole list, before the other names are written
  153. // to. A board that gave the same name to two channels has a name that means both
  154. // or neither, and resolving it to one of them is a retarget nobody asked for.
  155. func TestResolveZoneNameRefusesAWholeListOverOneAmbiguousName(t *testing.T) {
  156. _, err := resolveZoneName("logo,backlight", map[uint16]string{2: "logo", 3: "backlight", 4: "backlight"}, nil)
  157. if err == nil {
  158. t.Fatal("resolveZoneName() = nil error, want the ambiguous name refused")
  159. }
  160. if !strings.Contains(err.Error(), "several channels") {
  161. t.Errorf("error = %q, want it to say the name is ambiguous", err)
  162. }
  163. }
  164. func TestDisplayNameConflictsRejectsAShadowedSubsystemName(t *testing.T) {
  165. // Channel 1 is present and its subsystem is "backlight", while the file
  166. // also calls channel 3 "backlight": two channels, one name.
  167. err := displayNameConflicts(
  168. map[uint16]string{3: "backlight"},
  169. []via.Channel{via.ChannelBacklight, via.ChannelRgbMatrix},
  170. )
  171. if err == nil {
  172. t.Fatal("displayNameConflicts() expected an error, got nil")
  173. }
  174. if !strings.Contains(err.Error(), "backlight") {
  175. t.Errorf("error = %q, want it to name the conflicting name", err)
  176. }
  177. }
  178. func TestDisplayNameConflictsAllowsTheImpact80Naming(t *testing.T) {
  179. // The Impact 80 calls channel 3 "backlight" and has no channel 1, so
  180. // nothing shadows anything.
  181. err := displayNameConflicts(
  182. map[uint16]string{2: "logo", 3: "backlight", 4: "side"},
  183. []via.Channel{via.ChannelRgblight, via.ChannelRgbMatrix, via.ChannelAudio},
  184. )
  185. if err != nil {
  186. t.Fatalf("displayNameConflicts() error = %v, want nil", err)
  187. }
  188. }
  189. func TestChannelNamePrefersTheDisplayName(t *testing.T) {
  190. display := map[uint16]string{2: "logo"}
  191. if got := channelName(via.ChannelRgblight, display); got != "logo" {
  192. t.Errorf("channelName(2) = %q, want %q", got, "logo")
  193. }
  194. if got := channelName(via.ChannelRgbMatrix, display); got != "rgb_matrix" {
  195. t.Errorf("channelName(3) = %q, want %q", got, "rgb_matrix")
  196. }
  197. }
  198. // The display names must come from the keyboard the command targets, not from
  199. // whichever one enumeration returned first. They come from that board's
  200. // definition file, which is where a channel's name lives now.
  201. func TestPrepareTargetUsesTheSelectedKeyboard(t *testing.T) {
  202. devices := []intdevice.Device{
  203. {VendorID: 0x6666, ProductID: 0x0001},
  204. {VendorID: 0x36B0, ProductID: 0x309F, Name: "Impact 80"},
  205. }
  206. dir := t.TempDir()
  207. writeDefinition(t, dir, "rainy75.json", `{
  208. "name": "Rainy 75", "vendorId": "0x6666", "productId": "0x0001",
  209. "menus": [{"label":"Lighting","content":[{"label":"deck","content":[
  210. {"label":"Effect","type":"dropdown","content":["id_qmk_rgblight_effect",2,2],"options":["none"]}]}]}]}`)
  211. writeDefinition(t, dir, "impact80.json", `{
  212. "name": "Impact 80", "vendorId": "0x36B0", "productId": "0x309F",
  213. "menus": [{"label":"Lighting","content":[{"label":"logo","content":[
  214. {"label":"Effect","type":"dropdown","content":["id_qmk_rgblight_effect",2,2],"options":["none","wave"]}]}]}]}`)
  215. t.Cleanup(forceDefinitionsDir(t, dir))
  216. originalDiscover := discoverAll
  217. originalTarget := targetDevice
  218. t.Cleanup(func() {
  219. discoverAll = originalDiscover
  220. targetDevice = originalTarget
  221. })
  222. discoverAll = func() ([]intdevice.Device, error) { return devices, nil }
  223. targetDevice = "2"
  224. got, err := prepareTarget("logo")
  225. if err != nil {
  226. t.Fatalf("prepareTarget() error = %v", err)
  227. }
  228. if got.Display[2] != "logo" {
  229. t.Errorf("display names = %v, want the Impact 80's", got.Display)
  230. }
  231. if got.Device.Name != "Impact 80" {
  232. t.Errorf("name = %q, want the name the definition gives the board", got.Device.Name)
  233. }
  234. }
  235. // A board renamed after a profile was written leaves keys that resolve to
  236. // nothing. Each one must be reported by name, and nothing may be written.
  237. func TestLoadWarnsForEveryUnresolvableZoneKey(t *testing.T) {
  238. t.Cleanup(vendoredDefinitions(t))
  239. dir := t.TempDir()
  240. profile := Profile{
  241. Name: "renamed",
  242. Version: 1,
  243. Zones: map[string]*ZoneSettings{
  244. "old-logo": {Enabled: true, Effect: "light", Brightness: 100, Speed: 1, Color: "00ff"},
  245. "old-backlight": {Enabled: true, Effect: "wave", Brightness: 100, Speed: 1, Color: "00ff"},
  246. },
  247. }
  248. originalDir := profilesDirOverride
  249. t.Cleanup(func() { profilesDirOverride = originalDir })
  250. profilesDirOverride = dir
  251. if err := profile.Save(); err != nil {
  252. t.Fatalf("save profile: %v", err)
  253. }
  254. proto := &verifyingProtocol{}
  255. t.Cleanup(stubTargetForProfileTest(t, proto, "", impact80Display()))
  256. var stderr bytes.Buffer
  257. cmd := NewProfileLoadCmd()
  258. cmd.SetOut(&bytes.Buffer{})
  259. cmd.SetErr(&stderr)
  260. cmd.SetArgs([]string{"renamed"})
  261. if err := cmd.Execute(); err != nil {
  262. t.Fatalf("load returned error: %v", err)
  263. }
  264. for _, want := range []string{"old-logo", "old-backlight"} {
  265. if !strings.Contains(stderr.String(), want) {
  266. t.Errorf("stderr = %q, want it to name the unresolved key %q", stderr.String(), want)
  267. }
  268. }
  269. if len(proto.reports) != 0 {
  270. t.Errorf("reports = %v, want nothing written for unresolvable keys", proto.reports)
  271. }
  272. }
  273. // stubTargetForProfileTest points the profile commands at one protocol, one
  274. // profile directory and one zone, and returns the restore function.
  275. func stubTargetForProfileTest(t *testing.T, proto rgbProtocol, zone string, display map[uint16]string) func() {
  276. t.Helper()
  277. originalTarget := openTarget
  278. openTarget = func(requested string) (rgbProtocol, targetDeviceData, []via.Channel, error) {
  279. // The board identity matters: the catalog is selected by VID/PID, and a
  280. // profile that names an effect needs one to resolve it against.
  281. target := targetDeviceData{
  282. Device: intdevice.Device{VendorID: 0x36B0, ProductID: 0x309F},
  283. Display: display,
  284. }
  285. // A profile test says which zone the command was given; the seam falls
  286. // back to the one the test fixed so the stub works either way.
  287. if requested == "" {
  288. requested = zone
  289. }
  290. channels, err := resolveZoneName(requested, display, nil)
  291. if err != nil {
  292. return nil, target, nil, err
  293. }
  294. target.Requested = channels
  295. resolved, err := resolveChannels(proto, target)
  296. if err != nil {
  297. return nil, target, nil, err
  298. }
  299. return proto, target, resolved, nil
  300. }
  301. return func() { openTarget = originalTarget }
  302. }
  303. // A key that names a channel this keyboard does not have must be reported like
  304. // any other key it cannot place. "backlight" is the case that matters: it is a
  305. // display name on the Impact 80 and QMK's subsystem name for channel 1, so it
  306. // still resolves after a board renames channel 3 — to a channel that is absent.
  307. func TestLoadWarnsWhenAKeyNamesAnAbsentChannel(t *testing.T) {
  308. t.Cleanup(vendoredDefinitions(t))
  309. dir := t.TempDir()
  310. originalDir := profilesDirOverride
  311. t.Cleanup(func() { profilesDirOverride = originalDir })
  312. profilesDirOverride = dir
  313. profile := Profile{
  314. Name: "renamed",
  315. Version: 1,
  316. Zones: map[string]*ZoneSettings{
  317. "backlight": {Enabled: true, Effect: "light", Brightness: 100, Speed: 1, Color: "00ff"},
  318. },
  319. }
  320. if err := profile.Save(); err != nil {
  321. t.Fatalf("save profile: %v", err)
  322. }
  323. proto := &verifyingProtocol{}
  324. t.Cleanup(stubTargetForProfileTest(t, proto, "", nil))
  325. var stderr bytes.Buffer
  326. cmd := NewProfileLoadCmd()
  327. cmd.SetOut(&bytes.Buffer{})
  328. cmd.SetErr(&stderr)
  329. cmd.SetArgs([]string{"renamed"})
  330. if err := cmd.Execute(); err != nil {
  331. t.Fatalf("load returned error: %v", err)
  332. }
  333. if !strings.Contains(stderr.String(), "backlight") {
  334. t.Errorf("stderr = %q, want the key reported by name", stderr.String())
  335. }
  336. if len(proto.reports) != 0 {
  337. t.Errorf("reports = %v, want nothing written for an absent channel", proto.reports)
  338. }
  339. }
  340. // Without a catalog there are no effect names to look the profile's value up
  341. // in. The load must say that, not claim the name was not found.
  342. func TestLoadSaysSoWhenTheBoardHasNoCatalog(t *testing.T) {
  343. dir := t.TempDir()
  344. originalDir := profilesDirOverride
  345. t.Cleanup(func() { profilesDirOverride = originalDir })
  346. profilesDirOverride = dir
  347. profile := Profile{
  348. Name: "p",
  349. Version: 1,
  350. Zones: map[string]*ZoneSettings{
  351. "rgb_matrix": {Enabled: true, Effect: "breathing", Brightness: 100, Speed: 1, Color: "00ff"},
  352. },
  353. }
  354. if err := profile.Save(); err != nil {
  355. t.Fatalf("save profile: %v", err)
  356. }
  357. proto := &verifyingProtocol{applied: map[via.Channel]uint8{
  358. via.ChannelRgblight: 160,
  359. via.ChannelRgbMatrix: 255,
  360. via.ChannelAudio: 160,
  361. }}
  362. restore := stubTargetForUnknownBoard(t, proto, dir)
  363. t.Cleanup(restore)
  364. var stderr bytes.Buffer
  365. cmd := NewProfileLoadCmd()
  366. cmd.SetOut(&bytes.Buffer{})
  367. cmd.SetErr(&stderr)
  368. cmd.SetArgs([]string{"p"})
  369. if err := cmd.Execute(); err != nil {
  370. t.Fatalf("load returned error: %v", err)
  371. }
  372. if !strings.Contains(stderr.String(), "no effect names") {
  373. t.Errorf("stderr = %q, want the missing names named", stderr.String())
  374. }
  375. if strings.Contains(stderr.String(), `effect "breathing" not found`) {
  376. t.Errorf("stderr = %q, must not blame the effect name for a missing catalog", stderr.String())
  377. }
  378. if len(proto.reports) != 0 {
  379. t.Errorf("reports = %v, want nothing written without a catalog", proto.reports)
  380. }
  381. }
  382. // stubTargetForUnknownBoard points the commands at a board that has channels but
  383. // no definition and no compiled-in catalog, so it has no effect names.
  384. func stubTargetForUnknownBoard(t *testing.T, proto rgbProtocol, dir string) func() {
  385. t.Helper()
  386. originalTarget := openTarget
  387. originalDir := profilesDirOverride
  388. openTarget = func(string) (rgbProtocol, targetDeviceData, []via.Channel, error) {
  389. return proto, targetDeviceData{
  390. Device: intdevice.Device{VendorID: 0x6666, ProductID: 0x0001},
  391. }, impact80Channels(), nil
  392. }
  393. profilesDirOverride = dir
  394. return func() {
  395. openTarget = originalTarget
  396. profilesDirOverride = originalDir
  397. }
  398. }
  399. // A board without a catalog cannot name its effects, so a saved profile records
  400. // "unknown" for every channel and cannot be loaded back. That is worth saying at
  401. // save time, where the user can still do something about it.
  402. func TestSaveWarnsThatEffectNamesCannotBeRecorded(t *testing.T) {
  403. dir := t.TempDir()
  404. originalDir := profilesDirOverride
  405. t.Cleanup(func() { profilesDirOverride = originalDir })
  406. profilesDirOverride = dir
  407. proto := &verifyingProtocol{applied: map[via.Channel]uint8{
  408. via.ChannelRgblight: 160,
  409. via.ChannelRgbMatrix: 255,
  410. via.ChannelAudio: 160,
  411. }}
  412. restore := stubTargetForUnknownBoard(t, proto, dir)
  413. t.Cleanup(restore)
  414. var stderr bytes.Buffer
  415. cmd := NewProfileSaveCmd()
  416. cmd.SetOut(&bytes.Buffer{})
  417. cmd.SetErr(&stderr)
  418. cmd.SetArgs([]string{"p"})
  419. if err := cmd.Execute(); err != nil {
  420. t.Fatalf("save returned error: %v", err)
  421. }
  422. if !strings.Contains(stderr.String(), "unknown") {
  423. t.Errorf("stderr = %q, want the effect name it cannot record named", stderr.String())
  424. }
  425. if !strings.Contains(stderr.String(), "no effect names") {
  426. t.Errorf("stderr = %q, want the missing names named", stderr.String())
  427. }
  428. // The way out it names has to run. `effect` takes the zone as its first
  429. // argument, so a bare `effect <index>` would be read as a channel name.
  430. if !strings.Contains(stderr.String(), "effect <zone> <index>") {
  431. t.Errorf("stderr = %q, want the way out to name the zone as well as the ID", stderr.String())
  432. }
  433. }
  434. // A channel's own definition label and the subsystem name it replaces can both
  435. // match one name, and that is one channel rather than a conflict: the
  436. // documented `brightness backlight 100` has to keep working on a board whose
  437. // definition calls the channel "Backlight".
  438. func TestResolveZoneNameAcceptsTheNameADefinitionReplaced(t *testing.T) {
  439. display := map[uint16]string{2: "logo", 3: "Backlight", 4: "side"}
  440. alternatives := map[uint16][]string{3: {"backlight"}}
  441. for _, name := range []string{"backlight", "Backlight", "BACKLIGHT"} {
  442. got, err := resolveZoneName(name, display, alternatives)
  443. if err != nil {
  444. t.Errorf("resolveZoneName(%q) error = %v", name, err)
  445. continue
  446. }
  447. if len(got) != 1 || got[0] != 3 {
  448. t.Errorf("resolveZoneName(%q) = %v, want [3]", name, got)
  449. }
  450. }
  451. }
  452. // Two different channels answering to one name is still a conflict, whichever
  453. // source the names come from.
  454. func TestResolveZoneNameStillReportsARealConflict(t *testing.T) {
  455. display := map[uint16]string{2: "Backlight", 3: "backlight"}
  456. if _, err := resolveZoneName("backlight", display, map[uint16][]string{4: {"backlight"}}); err == nil {
  457. t.Error("resolveZoneName() = nil error, want a conflict for one name on two channels")
  458. }
  459. }
  460. // A board whose definition calls channel 3 "Backlight" has no channel 1, and
  461. // naming an unused channel would make "backlight" reach two channels at once —
  462. // the subsystem name of the one it does not have, and the label of the one it
  463. // does. The label has to win, because it is what the board calls that channel.
  464. func TestZoneNamePrefersTheDefinitionLabelOverAnotherChannelsSubsystem(t *testing.T) {
  465. dir := t.TempDir()
  466. writeDefinition(t, dir, "impact80.json", `{
  467. "name": "Impact 80", "vendorId": "0x36B0", "productId": "0x309F",
  468. "menus": [{"label":"Lighting","content":[{"label":"Backlight","content":[
  469. {"label":"Effect","type":"dropdown","content":["id_qmk_rgb_matrix_effect",3,2],
  470. "options":[["cycle_left_right",13]]}]}]}]}`)
  471. t.Cleanup(forceDefinitionsDir(t, dir))
  472. display, alternatives := applyDefinitionLabels(0x36B0, 0x309F)
  473. if display[3] != "Backlight" {
  474. t.Errorf("display = %v, want channel 3 named by the definition", display)
  475. }
  476. if _, ok := display[1]; ok {
  477. t.Errorf("display = %v, want no entry for a channel the definition does not name", display)
  478. }
  479. for _, name := range []string{"backlight", "Backlight", "BACKLIGHT", "rgb_matrix"} {
  480. got, err := resolveZoneName(name, display, alternatives)
  481. if err != nil {
  482. t.Errorf("resolveZoneName(%q) error = %v, want channel 3", name, err)
  483. continue
  484. }
  485. if len(got) != 1 || got[0] != 3 {
  486. t.Errorf("resolveZoneName(%q) = %v, want [3]", name, got)
  487. }
  488. }
  489. }
  490. // Fifteen commands in one flat list is where a user starts reading the help to
  491. // find a command and gives up. The groups say what a command is for, and a
  492. // command that belongs to none of them is a hole in the list.
  493. func TestRootHelpGroupsTheCommands(t *testing.T) {
  494. // The same registration the binary runs, so a command that someone adds
  495. // without a group is caught here rather than in the help output.
  496. root := newRootCommand()
  497. registerCommands(root)
  498. var out bytes.Buffer
  499. root.SetOut(&out)
  500. root.SetArgs([]string{"--help"})
  501. if err := root.Execute(); err != nil {
  502. t.Fatalf("--help returned error: %v", err)
  503. }
  504. help := out.String()
  505. for _, want := range []string{
  506. "Lighting Commands:",
  507. "Profile Commands:",
  508. "Keyboard Commands:",
  509. "save", "load", "list", "delete", "effect", "keyboard",
  510. } {
  511. if !strings.Contains(help, want) {
  512. t.Errorf("--help does not mention %q", want)
  513. }
  514. }
  515. // Every command the tool adds has to be in a group, or it lands in
  516. // "Additional Commands" where the grouping says nothing about it.
  517. grouped := make(map[string]bool)
  518. for _, c := range root.Commands() {
  519. if c.GroupID != "" {
  520. grouped[c.Name()] = true
  521. }
  522. }
  523. for _, c := range root.Commands() {
  524. if c.Name() == "help" || c.Name() == "completion" {
  525. continue
  526. }
  527. if !grouped[c.Name()] {
  528. t.Errorf("command %q is in no group, so it appears ungrouped in --help", c.Name())
  529. }
  530. }
  531. }