device_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package device
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "netdome.biz/paul/qmk-rgb/internal/hid"
  7. )
  8. func TestIndexDevices(t *testing.T) {
  9. tests := []struct {
  10. name string
  11. devices []Device
  12. want []string
  13. }{
  14. {
  15. name: "empty",
  16. devices: nil,
  17. want: nil,
  18. },
  19. {
  20. name: "single",
  21. devices: []Device{{Path: "/dev/hidraw7", VendorID: 0x36b0, ProductID: 0x309f}},
  22. want: []string{"/dev/hidraw7"},
  23. },
  24. {
  25. name: "sorted by vendor then product then path",
  26. devices: []Device{
  27. {Path: "/dev/hidraw9", VendorID: 0x9999, ProductID: 0x0001},
  28. {Path: "/dev/hidraw5", VendorID: 0x1111, ProductID: 0x0002},
  29. {Path: "/dev/hidraw2", VendorID: 0x1111, ProductID: 0x0001},
  30. },
  31. want: []string{"/dev/hidraw2", "/dev/hidraw5", "/dev/hidraw9"},
  32. },
  33. {
  34. name: "identical vendor and product sorted by path",
  35. devices: []Device{
  36. {Path: "/dev/hidrawB", VendorID: 0x36b0, ProductID: 0x309f},
  37. {Path: "/dev/hidrawA", VendorID: 0x36b0, ProductID: 0x309f},
  38. },
  39. want: []string{"/dev/hidrawA", "/dev/hidrawB"},
  40. },
  41. }
  42. for _, tt := range tests {
  43. t.Run(tt.name, func(t *testing.T) {
  44. got := indexDevices(tt.devices)
  45. if len(got) != len(tt.want) {
  46. t.Fatalf("indexDevices() returned %d devices, want %d", len(got), len(tt.want))
  47. }
  48. for i, wantPath := range tt.want {
  49. if got[i].Path != wantPath {
  50. t.Errorf("indexDevices()[%d].Path = %q, want %q", i, got[i].Path, wantPath)
  51. }
  52. if got[i].Index != i+1 {
  53. t.Errorf("indexDevices()[%d].Index = %d, want %d", i, got[i].Index, i+1)
  54. }
  55. }
  56. })
  57. }
  58. }
  59. func TestIndexDevicesDoesNotMutateInput(t *testing.T) {
  60. input := []Device{
  61. {Path: "/dev/hidraw9", VendorID: 0x9999, ProductID: 0x0001},
  62. {Path: "/dev/hidraw2", VendorID: 0x1111, ProductID: 0x0001},
  63. }
  64. indexDevices(input)
  65. if input[0].Path != "/dev/hidraw9" {
  66. t.Errorf("indexDevices() mutated input: input[0].Path = %q, want %q", input[0].Path, "/dev/hidraw9")
  67. }
  68. if input[0].Index != 0 {
  69. t.Errorf("indexDevices() mutated input: input[0].Index = %d, want 0", input[0].Index)
  70. }
  71. }
  72. func TestDeviceJSONFieldNames(t *testing.T) {
  73. dev := Device{
  74. Index: 1,
  75. Path: "/dev/hidraw7",
  76. VendorID: 0x36b0,
  77. ProductID: 0x309f,
  78. Name: "Impact 80",
  79. }
  80. data, err := json.Marshal(dev)
  81. if err != nil {
  82. t.Fatalf("Marshal() returned error: %v", err)
  83. }
  84. var got map[string]any
  85. if err := json.Unmarshal(data, &got); err != nil {
  86. t.Fatalf("Unmarshal() returned error: %v", err)
  87. }
  88. for _, field := range []string{"index", "path", "vendorId", "productId", "name"} {
  89. if _, ok := got[field]; !ok {
  90. t.Errorf("Device JSON = %s, missing field %q", data, field)
  91. }
  92. }
  93. // The field "known" used to say whether a data file listed the board. It is gone:
  94. // whether this tool has effect names for a board is reported by the info
  95. // command, which can look, instead of by a file that can go stale.
  96. if _, ok := got["known"]; ok {
  97. t.Errorf("Device JSON = %s, must not contain the removed field \"known\"", data)
  98. }
  99. if _, ok := got["supported"]; ok {
  100. t.Errorf("Device JSON = %s, must not contain the misleading field \"supported\"", data)
  101. }
  102. }
  103. // A keyboard that reports no USB product string has no name, and the field is
  104. // omitted rather than sent empty: a consumer can tell "no name" from a name.
  105. func TestDeviceJSONOmitsUnnamedName(t *testing.T) {
  106. data, err := json.Marshal(Device{Index: 2, Path: "/dev/hidraw9"})
  107. if err != nil {
  108. t.Fatalf("Marshal() returned error: %v", err)
  109. }
  110. if strings.Contains(string(data), `"name"`) {
  111. t.Errorf("Device JSON = %s, want the name omitted for a keyboard that reports none", data)
  112. }
  113. }
  114. // The name is the keyboard's own, from the USB product string, so a data file
  115. // cannot go stale and disagree with the hardware.
  116. func TestDeviceTakesTheNameFromTheProductString(t *testing.T) {
  117. d := Device{Index: 1, Path: "p", VendorID: 0x36B0, ProductID: 0x309F, Name: "Impact 80"}
  118. if d.Name != "Impact 80" {
  119. t.Errorf("Name = %q, want the product string the keyboard reports", d.Name)
  120. }
  121. }
  122. // The passed-over devices are what an empty discovery result has to explain, so
  123. // the grouping is the whole of the decision: one line per device rather than one
  124. // per collection, the raw HID collections left out, and a device that reports the
  125. // same collection twice listed once.
  126. func TestGroupOtherPerDevice(t *testing.T) {
  127. infos := []hid.DeviceInfo{
  128. {Path: "a", VendorID: 0x320f, ProductID: 0x5044, ProductString: "GMMK Pro",
  129. UsagePage: 0x0001, Usage: 0x06},
  130. {Path: "a", VendorID: 0x320f, ProductID: 0x5044, ProductString: "GMMK Pro",
  131. UsagePage: 0x000c, Usage: 0x01},
  132. // The same pair a second time, as the HID layer reports when a device
  133. // has several interfaces: one line about it says nothing.
  134. {Path: "a2", VendorID: 0x320f, ProductID: 0x5044, ProductString: "GMMK Pro",
  135. UsagePage: 0x0001, Usage: 0x06},
  136. // A board that put its lighting on a vendor page this tool does not
  137. // address. The page is the finding, so it has to survive the grouping.
  138. {Path: "b", VendorID: 0x320f, ProductID: 0x5044, ProductString: "GMMK Pro",
  139. UsagePage: 0xff80, Usage: 0x61},
  140. // The collection this tool drives, which belongs to DiscoverAll.
  141. {Path: "c", VendorID: 0x36b0, ProductID: 0x309f, ProductString: "Impact 80",
  142. UsagePage: 0xff60, Usage: 0x61, RawHID: true},
  143. }
  144. got := groupOther(infos)
  145. if len(got) != 1 {
  146. t.Fatalf("groupOther() = %d devices, want 1: %+v", len(got), got)
  147. }
  148. if got[0].VendorID != 0x320f || got[0].ProductID != 0x5044 {
  149. t.Errorf("vendor/product = %04X/%04X, want 320F/5044", got[0].VendorID, got[0].ProductID)
  150. }
  151. if got[0].Name != "GMMK Pro" {
  152. t.Errorf("name = %q, want %q", got[0].Name, "GMMK Pro")
  153. }
  154. want := []UsagePair{{0x0001, 0x06}, {0x000c, 0x01}, {0xff80, 0x61}}
  155. if len(got[0].UsagePairs) != len(want) {
  156. t.Fatalf("usage pages = %+v, want %+v", got[0].UsagePairs, want)
  157. }
  158. for i, w := range want {
  159. if got[0].UsagePairs[i] != w {
  160. t.Errorf("usagePages[%d] = %+v, want %+v", i, got[0].UsagePairs[i], w)
  161. }
  162. }
  163. }
  164. // A device with no product string still has to be listed, or the one board that
  165. // states nothing about itself is the one that goes missing from the explanation.
  166. func TestGroupOtherKeepsADeviceWithoutAName(t *testing.T) {
  167. got := groupOther([]hid.DeviceInfo{{VendorID: 0x0000, ProductID: 0x0000, UsagePage: 0xff00, Usage: 0xff}})
  168. if len(got) != 1 {
  169. t.Fatalf("groupOther() = %d devices, want 1", len(got))
  170. }
  171. if got[0].Name != "" {
  172. t.Errorf("name = %q, want it empty so the command can say so", got[0].Name)
  173. }
  174. }