definition_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. package rgb
  2. import (
  3. "os"
  4. "path/filepath"
  5. "testing"
  6. "netdome.biz/paul/qmk-rgb/internal/via"
  7. )
  8. // A definition may attach a number to an option that is not its position in the
  9. // list, so the ID has to come from the number and not from the index. This
  10. // fixture names three effects at IDs 1, 2 and 5 and leaves 0, 3 and 4 unnamed.
  11. func TestLoadDefinitionReadsEffectIDsFromTheOptions(t *testing.T) {
  12. def := loadDefinitionFixture(t, "tuple_options.json")
  13. effects := def.Catalog.Effects(via.ChannelRgbMatrix)
  14. if len(effects) != 3 {
  15. t.Fatalf("rgb_matrix effects = %d, want 3", len(effects))
  16. }
  17. want := []Effect{{ID: 1, Name: "Solid Color"}, {ID: 2, Name: "Breathing"}, {ID: 5, Name: "Rainbow"}}
  18. for i, w := range want {
  19. if effects[i] != w {
  20. t.Errorf("effects[%d] = %+v, want %+v", i, effects[i], w)
  21. }
  22. }
  23. // A gap must report unknown rather than the next name along.
  24. for _, id := range []uint8{0, 3, 4, 6} {
  25. if got := def.Catalog.EffectName(via.ChannelRgbMatrix, id); got != unknownEffectName {
  26. t.Errorf("EffectName(rgb_matrix, %d) = %q, want %q", id, got, unknownEffectName)
  27. }
  28. }
  29. }
  30. // The same effect is reachable by name, which is what `effect <name>` needs.
  31. func TestDefinitionEffectsResolveByName(t *testing.T) {
  32. def := loadDefinitionFixture(t, "tuple_options.json")
  33. id, ok := def.Catalog.EffectID(via.ChannelRgbMatrix, "Rainbow")
  34. if !ok || id != 5 {
  35. t.Errorf("EffectID(rgb_matrix, \"Rainbow\") = %d, %t, want 5, true", id, ok)
  36. }
  37. if _, ok := def.Catalog.EffectID(via.ChannelRgbMatrix, "Solid"); ok {
  38. t.Error("EffectID(rgb_matrix, \"Solid\") = found, want not found")
  39. }
  40. }
  41. // Plain string options carry no number, and the position is the ID.
  42. func TestLoadDefinitionNumbersStringOptionsByPosition(t *testing.T) {
  43. def := loadDefinitionFixture(t, "string_options.json")
  44. effects := def.Catalog.Effects(via.ChannelRgblight)
  45. want := []Effect{{ID: 0, Name: "All Off"}, {ID: 1, Name: "Solid Color"}, {ID: 2, Name: "Breathing 1"}, {ID: 3, Name: "Breathing 2"}}
  46. if len(effects) != len(want) {
  47. t.Fatalf("rgblight effects = %d, want %d", len(effects), len(want))
  48. }
  49. for i, w := range want {
  50. if effects[i] != w {
  51. t.Errorf("effects[%d] = %+v, want %+v", i, effects[i], w)
  52. }
  53. }
  54. }
  55. // VIA serves built definitions that carry vendorProductId as a number and no
  56. // vendorId/productId pair, so both spellings have to be read.
  57. func TestDefinitionIdentifiesTheBoardFromEitherSpelling(t *testing.T) {
  58. tests := []struct {
  59. file string
  60. wantVendor uint16
  61. wantProd uint16
  62. }{
  63. {"tuple_options.json", 0x1234, 0x5678},
  64. {"string_options.json", 0x1234, 0x5678},
  65. }
  66. for _, tt := range tests {
  67. def := loadDefinitionFixture(t, tt.file)
  68. if def.VendorID != tt.wantVendor || def.ProductID != tt.wantProd {
  69. t.Errorf("%s: vendor/product = 0x%04x/0x%04x, want 0x%04x/0x%04x",
  70. tt.file, def.VendorID, def.ProductID, tt.wantVendor, tt.wantProd)
  71. }
  72. }
  73. if got := loadDefinitionFixture(t, "string_options.json").Name; got != "Served Board" {
  74. t.Errorf("name = %q, want %q", got, "Served Board")
  75. }
  76. }
  77. // Brightness, color and speed controls are not effect lists and must not become
  78. // one.
  79. func TestLoadDefinitionIgnoresOtherControls(t *testing.T) {
  80. def := loadDefinitionFixture(t, "tuple_options.json")
  81. for _, ch := range []via.Channel{via.ChannelBacklight, via.ChannelRgbMatrix} {
  82. for _, e := range def.Catalog.Effects(ch) {
  83. if e.Name == "" {
  84. t.Errorf("channel %v has an effect without a name", ch)
  85. }
  86. }
  87. }
  88. if got := def.Catalog.Effects(via.ChannelRgbMatrix); len(got) != 3 {
  89. t.Errorf("rgb_matrix effects = %d, want 3 (brightness and color excluded)", len(got))
  90. }
  91. }
  92. // A definition is matched to a board by its VID/PID, so a directory can hold
  93. // many of them and the wrong one must not be used.
  94. func TestFindDefinitionMatchesTheBoard(t *testing.T) {
  95. dir := t.TempDir()
  96. copyFixture(t, dir, "tuple_options.json")
  97. defs, err := LoadDefinitionsDir(dir)
  98. if err != nil {
  99. t.Fatalf("LoadDefinitionsDir() error = %v", err)
  100. }
  101. if len(defs) != 1 {
  102. t.Fatalf("definitions = %d, want 1", len(defs))
  103. }
  104. if got := FindDefinition(defs, 0x1234, 0x5678); got == nil {
  105. t.Error("FindDefinition(matching) = nil, want the definition")
  106. }
  107. if got := FindDefinition(defs, 0x36B0, 0x309F); got != nil {
  108. t.Error("FindDefinition(other board) = a definition, want nil")
  109. }
  110. }
  111. // A file that is not a definition, or not one for any board, is reported rather
  112. // than skipped in silence.
  113. func TestLoadDefinitionsDirRejectsUnusableFiles(t *testing.T) {
  114. dir := t.TempDir()
  115. if err := os.WriteFile(filepath.Join(dir, "broken.json"), []byte("{not json"), 0o600); err != nil {
  116. t.Fatal(err)
  117. }
  118. if _, err := LoadDefinitionsDir(dir); err == nil {
  119. t.Error("LoadDefinitionsDir(broken json) = nil error, want an error")
  120. }
  121. }
  122. func TestLoadDefinitionRejectsAMissingFile(t *testing.T) {
  123. if _, err := LoadDefinition(filepath.Join(t.TempDir(), "nope.json")); err == nil {
  124. t.Error("LoadDefinition(missing) = nil error, want an error")
  125. }
  126. }
  127. func loadDefinitionFixture(t *testing.T, name string) *Definition {
  128. t.Helper()
  129. def, err := LoadDefinition(filepath.Join("testdata", name))
  130. if err != nil {
  131. t.Fatalf("LoadDefinition(%s) error = %v", name, err)
  132. }
  133. return def
  134. }
  135. func copyFixture(t *testing.T, dir, name string) {
  136. t.Helper()
  137. data, err := os.ReadFile(filepath.Join("testdata", name))
  138. if err != nil {
  139. t.Fatal(err)
  140. }
  141. if err := os.WriteFile(filepath.Join(dir, name), data, 0o600); err != nil {
  142. t.Fatal(err)
  143. }
  144. }
  145. // A definition carries the manufacturer's own spelling, and the tool's aliases
  146. // are a spelling convenience rather than board knowledge, so they have to work
  147. // whichever spelling a definition happens to use. The vendor of this board
  148. // writes "breathe" where the compiled-in catalog writes "breathing".
  149. func TestDefinitionCatalogResolvesTheToolsAliasesBothWays(t *testing.T) {
  150. names := []Effect{{ID: 0, Name: "none"}, {ID: 3, Name: "spectrum"}, {ID: 4, Name: "breathe"}, {ID: 5, Name: "light"}}
  151. catalog := NewCatalog("vendor", map[via.Channel][]Effect{via.ChannelRgblight: names})
  152. tests := []struct {
  153. name string
  154. effect string
  155. want uint8
  156. }{
  157. {"the vendor's own spelling", "breathe", 4},
  158. {"the tool's canonical name", "breathing", 4},
  159. {"an alias that the channel has", "off", 0},
  160. {"an alias that resolves per channel", "rainbow", 3},
  161. {"a name no alias or entry reaches", "nonsense", 0},
  162. }
  163. for _, tt := range tests {
  164. t.Run(tt.name, func(t *testing.T) {
  165. id, ok := catalog.EffectID(via.ChannelRgblight, tt.effect)
  166. if tt.effect == "nonsense" {
  167. if ok {
  168. t.Errorf("EffectID(%q) = %d, true, want not found", tt.effect, id)
  169. }
  170. return
  171. }
  172. if !ok || id != tt.want {
  173. t.Errorf("EffectID(%q) = %d, %t, want %d, true", tt.effect, id, ok, tt.want)
  174. }
  175. })
  176. }
  177. }
  178. // The compiled-in catalog keeps working exactly as before.
  179. func TestBuiltInCatalogStillResolvesItsAliases(t *testing.T) {
  180. catalog := impact80(t)
  181. tests := []struct {
  182. channel via.Channel
  183. effect string
  184. want uint8
  185. }{
  186. {via.ChannelRgblight, "breathing", 4},
  187. {via.ChannelRgblight, "breathe", 4},
  188. {via.ChannelRgblight, "off", 0},
  189. {via.ChannelRgbMatrix, "breathing", 5},
  190. {via.ChannelRgbMatrix, "rainbow", 17},
  191. {via.ChannelAudio, "rainbow", 3},
  192. }
  193. for _, tt := range tests {
  194. id, ok := catalog.EffectID(tt.channel, tt.effect)
  195. if !ok || id != tt.want {
  196. t.Errorf("EffectID(%v, %q) = %d, %t, want %d, true", tt.channel, tt.effect, id, ok, tt.want)
  197. }
  198. }
  199. }
  200. // A definition writes display spellings ("fixed wave") where the tool writes
  201. // identifiers ("fixed_wave"). The difference is whitespace, not a different
  202. // effect, so both must resolve — the tool's documented name may not stop working
  203. // because a definition file happens to be present.
  204. func TestDefinitionCatalogResolvesSpacesAsUnderscores(t *testing.T) {
  205. names := []Effect{{ID: 0, Name: "none"}, {ID: 1, Name: "wave"}, {ID: 2, Name: "fixed wave"}}
  206. catalog := NewCatalog("vendor", map[via.Channel][]Effect{via.ChannelRgblight: names})
  207. tests := []struct {
  208. effect string
  209. want uint8
  210. }{
  211. {"fixed wave", 2},
  212. {"fixed_wave", 2},
  213. {"wave", 1},
  214. }
  215. for _, tt := range tests {
  216. id, ok := catalog.EffectID(via.ChannelRgblight, tt.effect)
  217. if !ok || id != tt.want {
  218. t.Errorf("EffectID(%q) = %d, %t, want %d, true", tt.effect, id, ok, tt.want)
  219. }
  220. }
  221. }
  222. // The compiled-in catalog has no spaced names, so the rule changes nothing there.
  223. func TestBuiltInCatalogIsUnchangedByTheSpacingRule(t *testing.T) {
  224. catalog := impact80(t)
  225. if id, ok := catalog.EffectID(via.ChannelRgblight, "fixed_wave"); !ok || id != 2 {
  226. t.Errorf("EffectID(rgblight, \"fixed_wave\") = %d, %t, want 2, true", id, ok)
  227. }
  228. if _, ok := catalog.EffectID(via.ChannelRgbMatrix, "fixed_wave"); ok {
  229. t.Error("EffectID(rgb_matrix, \"fixed_wave\") = found, want not found: only the rgblight channel has it")
  230. }
  231. }
  232. // The sub-menu a definition puts a lighting channel under is that channel's name
  233. // in VIA's own interface, and it is board data the file already carries. Using
  234. // it means the tool and VIA call the same channel the same thing, instead of
  235. // the tool inventing a third name.
  236. func TestDefinitionNamesItsChannelsBySubMenu(t *testing.T) {
  237. def := loadDefinitionFixture(t, "tuple_options.json")
  238. want := map[uint16]string{2: "logo", 3: "Backlight", 4: "side"}
  239. for channel, label := range want {
  240. if got := def.Labels[channel]; got != label {
  241. t.Errorf("Labels[%d] = %q, want %q", channel, got, label)
  242. }
  243. }
  244. if len(def.Labels) != len(want) {
  245. t.Errorf("Labels = %v, want only the lighting channels", def.Labels)
  246. }
  247. }
  248. // The label is whatever the definition calls the channel, not a name this tool
  249. // knows: a different definition calls the same channel Underglow, and that name
  250. // is the one VIA shows.
  251. func TestDefinitionTakesWhateverTheChannelIsCalled(t *testing.T) {
  252. def := loadDefinitionFixture(t, "string_options.json")
  253. if got := def.Labels[2]; got != "Underglow" {
  254. t.Errorf("Labels[2] = %q, want %q", got, "Underglow")
  255. }
  256. if len(def.Labels) != 1 {
  257. t.Errorf("Labels = %v, want only the channel the definition names", def.Labels)
  258. }
  259. }
  260. // The definition files in the repository are read at runtime, so a truncated or
  261. // edited one would fail every command that names an effect rather than fail a
  262. // test. This parses the vendored Impact 80 file and checks what it has to
  263. // contain.
  264. func TestVendoredImpact80DefinitionIsUsable(t *testing.T) {
  265. def, err := LoadDefinition(filepath.Join("..", "..", "definitions", "impact80.json"))
  266. if err != nil {
  267. t.Fatalf("LoadDefinition(definitions/impact80.json) error = %v", err)
  268. }
  269. if def.VendorID != 0x36B0 || def.ProductID != 0x309F {
  270. t.Errorf("identifiers = 0x%04X/0x%04X, want 0x36B0/0x309F", def.VendorID, def.ProductID)
  271. }
  272. want := map[via.Channel]int{
  273. via.ChannelRgblight: 7,
  274. via.ChannelRgbMatrix: 46,
  275. via.ChannelAudio: 7,
  276. }
  277. for ch, count := range want {
  278. if got := len(def.Catalog.Effects(ch)); got != count {
  279. t.Errorf("channel %v has %d effects, want %d", ch, got, count)
  280. }
  281. }
  282. // The names are the vendor's, and the tool's spelling has to resolve too.
  283. if id, ok := def.Catalog.EffectID(via.ChannelRgblight, "fixed wave"); !ok || id != 2 {
  284. t.Errorf("EffectID(rgblight, \"fixed wave\") = %d, %t, want 2, true", id, ok)
  285. }
  286. if id, ok := def.Catalog.EffectID(via.ChannelRgblight, "fixed_wave"); !ok || id != 2 {
  287. t.Errorf("EffectID(rgblight, \"fixed_wave\") = %d, %t, want 2, true", id, ok)
  288. }
  289. if got := def.Labels[3]; got != "Backlight" {
  290. t.Errorf("Labels[3] = %q, want %q", got, "Backlight")
  291. }
  292. }