names_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. package rgb
  2. import (
  3. "os"
  4. "path/filepath"
  5. "testing"
  6. "netdome.biz/paul/qmk-rgb/internal/via"
  7. )
  8. func writeNames(t *testing.T, body string) *Names {
  9. t.Helper()
  10. path := filepath.Join(t.TempDir(), "board.json")
  11. if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
  12. t.Fatal(err)
  13. }
  14. names, err := LoadNamesFile(path)
  15. if err != nil {
  16. t.Fatalf("LoadNamesFile() error = %v", err)
  17. }
  18. return names
  19. }
  20. // A name the user wrote is theirs, and the tool has to be able to say so wherever
  21. // it prints the name: nothing in a file can be read back off a keyboard, so a name
  22. // is the one value it cannot verify, and a user's name is a fact about them.
  23. func TestNamesCarryTheirSource(t *testing.T) {
  24. names := writeNames(t, `{
  25. "vendorId": "0x1234", "productId": "0x5678",
  26. "channels": {"rgb_matrix": {"7": "rainbow_moving_chevron"}}
  27. }`)
  28. catalog := names.Apply(nil)
  29. if got := catalog.EffectName(via.ChannelRgbMatrix, 7); got != "rainbow_moving_chevron" {
  30. t.Errorf("EffectName() = %q, want the name the user wrote", got)
  31. }
  32. if got := catalog.EffectSource(via.ChannelRgbMatrix, 7); got != SourceUser {
  33. t.Errorf("EffectSource() = %q, want %q", got, SourceUser)
  34. }
  35. }
  36. // A board with no definition file is exactly the board a user writes names for, so
  37. // a names file alone has to produce a catalog. The manufacturer's file is the one
  38. // that is missing, not the names.
  39. func TestNamesAloneMakeACatalogForABoardWithNoDefinition(t *testing.T) {
  40. names := writeNames(t, `{
  41. "vendorId": "0x1234", "productId": "0x5678",
  42. "channels": {"rgblight": {"0": "none", "1": "wave"}}
  43. }`)
  44. catalog := names.Apply(nil)
  45. effects := catalog.Effects(via.ChannelRgblight)
  46. if len(effects) != 2 {
  47. t.Fatalf("effects = %v, want two", effects)
  48. }
  49. if effects[0].ID != 0 || effects[0].Name != "none" {
  50. t.Errorf("effects[0] = %v, want ID 0 named none", effects[0])
  51. }
  52. if effects[1].ID != 1 || effects[1].Name != "wave" {
  53. t.Errorf("effects[1] = %v, want ID 1 named wave", effects[1])
  54. }
  55. // A name the user wrote resolves back to its number, or naming it would be
  56. // decoration.
  57. id, ok := catalog.EffectID(via.ChannelRgblight, "wave")
  58. if !ok || id != 1 {
  59. t.Errorf("EffectID(\"wave\") = %d, %t, want 1, true", id, ok)
  60. }
  61. }
  62. // A user's name replaces the vendor's for that one effect and leaves the rest of
  63. // the file alone: overriding one name must not mean restating the ones that were
  64. // already right.
  65. func TestNamesOverrideOneEffectAndLeaveTheRest(t *testing.T) {
  66. base := NewCatalog("Test Board", map[via.Channel][]Effect{
  67. via.ChannelRgbMatrix: {
  68. {ID: 0, Name: "none"},
  69. {ID: 1, Name: "solid_color"},
  70. {ID: 2, Name: "breathing"},
  71. },
  72. })
  73. names := writeNames(t, `{
  74. "vendorId": "0x1234", "productId": "0x5678",
  75. "channels": {"rgb_matrix": {"2": "pulsing"}}
  76. }`)
  77. catalog := names.Apply(base)
  78. if got := catalog.EffectName(via.ChannelRgbMatrix, 2); got != "pulsing" {
  79. t.Errorf("EffectName(2) = %q, want the user's name", got)
  80. }
  81. if got := catalog.EffectSource(via.ChannelRgbMatrix, 2); got != SourceUser {
  82. t.Errorf("EffectSource(2) = %q, want %q", got, SourceUser)
  83. }
  84. if got := catalog.EffectName(via.ChannelRgbMatrix, 1); got != "solid_color" {
  85. t.Errorf("EffectName(1) = %q, want the vendor's name left alone", got)
  86. }
  87. if got := catalog.EffectSource(via.ChannelRgbMatrix, 1); got != SourceVendor {
  88. t.Errorf("EffectSource(1) = %q, want an empty source for the vendor's", got)
  89. }
  90. }
  91. // A definition may stop short of the board's highest ID, and a user naming that
  92. // one is telling the tool something the file did not. So the entry is added rather
  93. // than refused.
  94. func TestNamesAddAnEffectTheDefinitionDoesNotHave(t *testing.T) {
  95. base := NewCatalog("Test Board", map[via.Channel][]Effect{
  96. via.ChannelRgbMatrix: {{ID: 0, Name: "none"}},
  97. })
  98. names := writeNames(t, `{
  99. "vendorId": "0x1234", "productId": "0x5678",
  100. "channels": {"rgb_matrix": {"46": "riverflow"}}
  101. }`)
  102. catalog := names.Apply(base)
  103. effects := catalog.Effects(via.ChannelRgbMatrix)
  104. if len(effects) != 2 {
  105. t.Fatalf("effects = %v, want the definition's and the user's", effects)
  106. }
  107. if effects[1].ID != 46 || effects[1].Name != "riverflow" {
  108. t.Errorf("effects[1] = %v, want ID 46 named riverflow", effects[1])
  109. }
  110. }
  111. // Applying names must not reach back into the definition it was given, or a second
  112. // lookup for the same board would see the first one's overrides.
  113. func TestApplyDoesNotMutateTheCatalogItWasGiven(t *testing.T) {
  114. base := NewCatalog("Test Board", map[via.Channel][]Effect{
  115. via.ChannelRgbMatrix: {{ID: 1, Name: "solid_color"}},
  116. })
  117. names := writeNames(t, `{
  118. "vendorId": "0x1234", "productId": "0x5678",
  119. "channels": {"rgb_matrix": {"1": "steady"}}
  120. }`)
  121. names.Apply(base)
  122. if got := base.EffectName(via.ChannelRgbMatrix, 1); got != "solid_color" {
  123. t.Errorf("the definition's catalog now says %q, want it untouched", got)
  124. }
  125. }
  126. // A file that is not a names file is a user error worth reporting. Answering
  127. // "no names" instead would read as the keyboard having none.
  128. func TestLoadNamesFileRejectsAChannelThatIsNotOne(t *testing.T) {
  129. path := filepath.Join(t.TempDir(), "board.json")
  130. if err := os.WriteFile(path, []byte(`{
  131. "vendorId": "0x1234", "productId": "0x5678",
  132. "channels": {"keymap": {"1": "x"}}
  133. }`), 0o644); err != nil {
  134. t.Fatal(err)
  135. }
  136. if _, err := LoadNamesFile(path); err == nil {
  137. t.Error("LoadNamesFile() = nil error, want a channel that is not a lighting channel refused")
  138. }
  139. }
  140. func TestLoadNamesFileRejectsAnEffectIDThatIsNotOne(t *testing.T) {
  141. path := filepath.Join(t.TempDir(), "board.json")
  142. if err := os.WriteFile(path, []byte(`{
  143. "vendorId": "0x1234", "productId": "0x5678",
  144. "channels": {"rgb_matrix": {"two": "x"}}
  145. }`), 0o644); err != nil {
  146. t.Fatal(err)
  147. }
  148. if _, err := LoadNamesFile(path); err == nil {
  149. t.Error("LoadNamesFile() = nil error, want an effect ID that is not one refused")
  150. }
  151. }
  152. // An effect ID is a JSON object key, so it arrives as a string, and both spellings
  153. // the tool prints elsewhere are worth accepting.
  154. func TestEffectIDIsReadInEitherSpelling(t *testing.T) {
  155. tests := []struct {
  156. raw string
  157. want uint8
  158. }{
  159. {"7", 7},
  160. {"0x07", 7},
  161. {"46", 46},
  162. {"0x2E", 46},
  163. }
  164. for _, tt := range tests {
  165. got, err := parseEffectID(tt.raw)
  166. if err != nil {
  167. t.Errorf("parseEffectID(%q) error = %v", tt.raw, err)
  168. continue
  169. }
  170. if got != tt.want {
  171. t.Errorf("parseEffectID(%q) = %d, want %d", tt.raw, got, tt.want)
  172. }
  173. }
  174. }